Templates and fields
A template is a locked design plus a field schema. The schema is the contract you expose — it decides exactly what a caller may change, and nothing else can move.
Anatomy#
| Property | Meaning |
|---|---|
kind | image uses layers; video uses a timeline |
fields | What callers may set — the public contract |
layers | The image composition (locked) |
timeline | The video composition (locked) |
status | draft or published — only a published template can be rendered |
visibility | private or public — who may copy it |
categoryId | The main category. Required; a subcategory is optional |
version | Increments on every change; renders record which version they used |
Field types#
| Type | Accepts | Config options |
|---|---|---|
text | string | maxLength, minLength, pattern |
longtext | string | maxLength, minLength |
richtext | HTML string | maxLength |
number | number | min, max, step |
slider | number | min, max, step |
toggle | boolean | — |
select | string | options[] |
multiselect | string[] | options[] |
color | hex, rgb(), hsl() | palette, allowAlpha |
gradient | object | — |
image | URL or data URI | aspectRatio, minWidth, minHeight |
video | URL | maxDurationSec |
audio | URL | maxDurationSec |
lottie | URL to JSON | — |
date | ISO date string | format |
url / email / phone | string | validated by shape |
json / chart / table | object or JSON string | — |
qr | string | — |
Values are validated before anything is charged. Failures return 422 TEMPLATE_VALIDATION_FAILED with a per-field errors object, so you can fix an entire payload in one round trip rather than discovering problems one at a time.
date field pairs with the dynamic video assets — see Dynamic content in the video guide. Bind one to a countdown’s to and each render counts toward the deadline that render was given; bind it to a datetime’s value and the same template stamps a different date every time. Leave the asset at "now" instead and it needs no field at all — it takes the moment the render runs.Bindings#
A binding points a field at a property pathon a layer or clip. This is what makes templates open-ended: a field is not limited to swapping text or an image — it can drive font size, position, colour, a clip’s trim point, or a transition name.
{
"key": "headline",
"label": "Headline",
"type": "text",
"required": true,
"config": { "maxLength": 42 },
"bindings": [
{ "target": "title", "path": "text" },
{ "target": "shadow", "path": "text" },
{ "target": "title", "path": "style.fontSize",
"transform": "number", "factor": 0.9 }
]
}| Property | Meaning |
|---|---|
target | Layer id (image templates) or clip alias (video templates) |
path | Dot notation on the layer or clip — text, style.fontSize, asset.src, offset.x |
transform | uppercase, lowercase, titlecase, trim, number, boolean |
factor | Multiplier — maps a 0–100 slider onto real units |
offset | Added after the factor: value × factor + offset |
skipIfEmpty | Default true — an empty value leaves the design’s own value in place |
alias, not by position. That means reordering a timeline never silently breaks a template — an alias that no longer exists is skipped rather than writing to the wrong clip.Organising the editor#
Groups and order
group puts a field in a named panel; order sorts within it; advanced: true hides it behind a disclosure. The template’s fieldGroups array sets the panel order.
Conditional visibility
Show a field only when another has a particular value.
{
"key": "discountBadge",
"type": "text",
"visibleWhen": { "field": "showDiscount", "equals": true }
}
// Also supported:
// "visibleWhen": { "field": "layout", "in": ["hero", "split"] }
// "visibleWhen": { "field": "mode", "notEquals": "minimal" }Audience
editorOnly: true shows a field in the studio but hides it from the API schema. apiOnly: true does the reverse — useful for values your systems supply that a human should never type.
Image layers#
An image template is a stack of layers, painted in array order — the first is at the back. Every layer shares a frame (position, size, rotation, opacity, blend mode, effects); the rest of its properties depend on its type.
| Type | Draws |
|---|---|
text | A run of type. Also carries the {{variable}} highlight styling. |
date | Text, formatted from the render’s own clock via dateTimeFormat |
image | A picture, with fit, frame, adjustments and optional background removal |
shape | One of the built-in silhouettes, named by shapeType |
icon | Vector geometry, recoloured by fillColor — not a picture of an icon |
gradient | A colour wash — a scrim over a photo, or a full-bleed backdrop |
chart / table / widget | Data, rendered by the shared infographic engine |
The frame — every layer has these
| Property | Meaning |
|---|---|
x, y, width, height | Design pixels, in the template’s own coordinate space |
opacity | Accepted on either scale: 0–1 or 0–100. Anything at or below 1 is read as a fraction. |
rotation | Degrees, clockwise, about the layer’s centre |
flipX, flipY | Mirror the layer on either axis |
perspective | Map the layer onto four corners instead of its own box — { tl, tr, br, bl }, each an [x, y] pair given as a fraction of the layer. { tl: [0,0], tr: [1,0], br: [1,1], bl: [0,1] } is the box itself and changes nothing. This is what puts artwork onto an angled surface in a product mockup; pair it with surface below. Corners that cross or collapse are ignored rather than drawn. |
surface | Carry the light of the photo underneath across this layer, so artwork on a mockup sits under the product’s own folds and highlights instead of on top of them. { source, x, y, width, height, fit, rotation, shadow, highlight, displace, occlusion } — source is the image to take the light from and the box is where it sits, in design pixels, normally copied from the layer beneath. shadow and highlight are 0–100 and default to 100 and 45. The blend is confined to this layer, so the photo beneath is never darkened twice. displace (0–100, default 0) goes further and moves the artwork with the surface’s folds, so a straight line printed across a crease comes out bent — exact on a flat-on shot, and approximated from the print area’s bounding box when the layer is also pinned with perspective. occlusion is a cut-out of anything that passes in front of the artwork — a hand, a strap, a fold. It is drawn over the layer at normal blend, so a transparent PNG of the object is used directly, with no mask to invert. All of it is confined to the layer. |
blendMode | Any CSS blend — multiply, screen, overlay, and the rest |
effects | Shadow, glow, outline, blur and backdrop blur — see below |
visible, locked | Skip the layer entirely; freeze it in the editor |
Effects
effects applies to any layer type, not just pictures — the renderer puts it on the layer frame, above whatever the layer happens to draw. A glow follows the actual silhouette, so a glowing star is a glowing star rather than a glowing rectangle.
{
"effects": {
"shadow": { "enabled": true, "x": 0, "y": 20, "blur": 48,
"spread": -8, "color": "#0F172A", "opacity": 30 },
"glow": { "enabled": true, "blur": 24, "color": "#8B3DFF", "opacity": 80 },
"outline": { "enabled": true, "width": 3, "color": "#171C26" },
"blur": 0,
"backdropBlur": 18
}
}outlinestrokes the glyphs rather than bordering the box — that is what makes hollow display type possible. On a shape it is the shape’s own stroke, and it can be dashed or dotted.Shapes
shapeType names a silhouette. Rectangles and ellipses are drawn as boxes so borderRadius still applies to them; everything else is a path that stretches with the layer.
| Group | Values |
|---|---|
| Basic | rect, rounded, circle, ellipse, squircle |
| Geometric | triangle, triangle-down, diamond, pentagon, hexagon, hexagon-flat, octagon, parallelogram, trapezoid, cross, star5, star6, star8, burst |
| Organic | heart, drop, shield, cloud, blob1, blob2, blob3, semicircle, quarter, arch, ring |
| Arrows & banners | arrow-right, arrow-left, arrow-up, chevron, banner, ribbon |
| Callouts | speech, speech-round |
| Dimensional | iso-cube, iso-cylinder, sphere, coin, price-tag, gift, map-pin, megaphone-3d, bag-3d, rocket-3d, trophy-3d, crown-3d, gem-3d, bolt-3d, flame-3d, bell-3d, parcel-3d, card-3d, clock-3d, growth-3d, target-3d, percent-3d, cart-3d, play-3d |
| Celebration | confetti, confetti-colour, balloons, bunting, firework-colour, party-hat, streamer |
| Lines | rule, rule-vertical, rule-diagonal, rule-double, rule-arc, rule-curve, rule-s, rule-wave, rule-zigzag, rule-bracket, rule-corner |
| Connectors | connector-line, connector-elbow, connector-elbow-v, connector-step, connector-curve |
Lines and connectors
A line is a shape, but it is drawn as a strokerather than as a filled outline — which is what lets it carry a dash pattern, a cap and an arrowhead. The geometry is generated at the layer’s real size, so the thickness is even along the whole path and a dash stays the length it was asked for however long the line is.
| Property | Meaning |
|---|---|
lineWidth | Thickness in design pixels. Defaults to 3. |
lineStyle | solid, dashed, dotted, dash-dot, long-dash, fine. Dash lengths follow the thickness, so the pattern still reads on a heavy line. |
lineCap | butt, round, square |
lineStart, lineEnd | none, arrow, triangle, circle, square, diamond, bar. Sized against the thickness and rotated to follow the path, so a curved connector’s head points along the curve. |
connect | Connectors only: { from, to, fromSide, toSide }. from and to are layer ids; the sides are auto (the default), top, right, bottom or left. |
A pinned connector stores the relationship, not the coordinates. Its box is recomputed from the two elements immediately before layout, so an arrow between two cards stays between them even when a field moves one of them on this particular render. Pinning to a layer that no longer exists is not an error — the connector keeps the box it was drawn with.
The colour comes from fillColor, not borderColor: to whoever is reading the design there is one line colour, and the arrowheads are painted in it too.
Widgets, and binding data to them
A widget draws a single number: a progress bar, a ring, a gauge or a KPI. It has three colours, because it is three things — the filled part, the unfilled track behind it, and the type on top.
| Property | Meaning |
|---|---|
widgetType | progress, ring, gauge, kpi |
widgetValue, widgetMax | The number, and what it is out of. Defaults to 100. |
widgetLabel, widgetSuffix | Caption under the value, and what follows it — usually %. |
fillColor | The filled part — the bar, the arc. |
widgetTrackColor | The UNFILLED part. Defaults to a light slate. |
textColor | The value and its label. |
widgetTrackColor set — otherwise a pale band runs through it.Binding a field to a widget layer sends a value on every render, which is the point of putting one on a template. A plain number is enough — 72 or 72% sets widgetValue. A JSON object sets anything else on the layer, so one field can move the value and recolour it at once. A color field bound to the layer sets fillColor, the same as it does for a shape.
{ "completion": "72" }
// or, to set more than the value:
{ "completion": "{"widgetValue":72,"fillColor":"#00C853"}" }The same applies to chart and table layers: a field carrying JSON replaces their data, and a colour field recolours them.
Type and legends on a chart layer
A chart, table or widget layer is drawn by the same renderer the video engine uses, so the same JSON produces the same pixels as a still, in a video, or in the mobile app. Its labels are derived from fontSize — a title is that plus three, an axis tick one less — which is a good default and a poor ceiling. These fields are the ceiling coming off.
{
"type": "chart",
"chartType": "bar",
"chartTitle": "Enquiries by month",
"chartData": { "labels": ["Jun","Jul","Aug"], "values": [18,34,27] },
"chartFontScale": 1.5
}
// One number. Labels, ticks, the legend and its
// swatches all grow together, and the left gutter
// widens to fit the bigger ticks.| Property | Does |
|---|---|
chartFontScale | Multiplies every piece of type the layer draws — a table’s rows and a widget’s readout included. 1 is the chart as drawn. |
chartTextStyles | Per-role typography, keyed by role: title, axis, labels, values, legend, header, cell, value, caption. Each takes { family, size, scale, weight, color, opacity, letterSpacing, transform, align, offset, hidden }. |
chartLegend | { position, align, direction, swatch, swatchSize, gap, itemGap, rowGap, size }. position is one of top | bottom | left | right; the plot gives up the room rather than being drawn over. |
chartGridColor | The gridlines behind an axis plot. showGrid owns whether they exist. |
chartAxisTicks | Divisions on the value axis — 4 by default, so five labels counting zero. |
chartPadding | Extra room around the plot, as a number or { top, right, bottom, left } — added to what the chart works out for itself, so it stays right when the data changes. Negative tightens. |
chartValuePosition | auto | inside | outside — where showValues puts a bar’s number. auto keeps it outside unless it would overflow, then inverts it to white inside the bar. |
chart prefix (fontScale, text, legend, padding…) because a chart asset is already its own namespace there; an image layer’s text and padding are taken.Shadows, glow and depth
A shadow is cast by what the layer draws, not by its box. A path shape, an icon, a QR and text with no background behind it all cast the shadow of their own outline; a rectangle, a plain photo or a text box with a background casts a box shadow, which is the only one of the two that can take a spread or an inset.
| Property | Meaning |
|---|---|
effects.shadow | { enabled, x, y, blur, spread, color, opacity, inset, follow }. Offsets in design px; opacity is 0–100. |
effects.shadow.follow | box or shape, overriding the automatic choice. Set shape for a cut-out photograph, whose subject lives in the alpha channel and cannot be seen from the layer. |
effects.glow | { enabled, blur, color, opacity } — a halo, also cast by the outline |
effects.extrude | Text only. { enabled, depth, angle, color } — the solid side of a three-dimensional letter. angle is degrees, 90 being straight down; depth is in design px and capped at 80 steps. |
QR codes and barcodes
A code layer stores the settings, not a picture of a code — so binding a field to code.value makes one template produce a different, correct code on every render. That is the whole reason to have this rather than an uploaded image: a per-recipient link, an order number, a ticket.
| Property | Meaning |
|---|---|
code.type | qrcode, gs1qrcode, datamatrix, azteccode, pdf417, code128, code39, ean13, ean8, upca, itf14, gs1-128 |
code.value | What it encodes. Usually driven by a field. |
code.color, code.background | Hex. A null background leaves it transparent. |
code.eccLevel | QR only: L, M, Q, H — roughly 7%, 15%, 25% and 30% of the code may be covered and still scan. |
code.moduleShape | 2D codes: square, dot or rounded |
code.margin | Quiet zone in modules. Defaults to 4, and never goes below 2. |
code.showText | Linear barcodes: print the value underneath. |
Recolouring artwork that has several colours
Most shapes take one fillColor, and dimensional ones derive their lit and shaded faces from it. Some artwork is multicoloured on purpose — a confetti scatter, a bunting, a two-tone icon — and for those a single fill would only flatten the thing that makes it work.
paletteis an array of colours, positional against the element’s own palette in painting order. An entry that is nullor missing keeps the artwork’s original colour, and anything that is not a valid CSS colour is ignored rather than drawn. Repeated colours count once: a bunting with twelve flags in six colours takes six entries, and changing the second recolours every flag painted in it.
{
"type": "shape",
"shapeType": "bunting",
"palette": ["#8B3DFF", null, "#00C4CC"]
}icon, whose colours are read out of its markup — those icons paint with literal fills instead of currentColor, so fillColor has nothing to tint.Pictures
| Property | Meaning |
|---|---|
imageUri | URL or data URI. Usually driven by an image field. |
objectFit | cover, contain, fill, scale-down, none |
frame | Any shape id above — masks the picture to that silhouette |
adjustments | brightness, contrast, saturation (100 is neutral), hue in degrees, plus blur, grayscale, sepia, invert |
removeBackground | Cut the subject out at render time — see below |
tint, tintOpacity, tintBlend | A flat colour laid over the picture |
removeBackground is set on the LAYER, not baked into the asset, because the picture usually is not known when the template is authored — a product field supplies a different photo on every render. The cut-out therefore runs on whatever image the render was given.
warnings — a background that did not come off is a disappointment, not a reason to fail a render you were charged for.Icons
An icon is stored as geometry, not as a picture. A rasterised icon cannot be recoloured to match a palette and softens the moment it is scaled past its capture size, and those are the two things people do to icons constantly.
| Property | Meaning |
|---|---|
iconBody | The inner SVG markup — paths and shapes only. Sanitised to a strict allowlist on the way in. |
iconViewBox | The source coordinate space, e.g. 0 0 24 24 |
fillColor | Tints the whole icon, via currentColor |
gradient | Paints the icon from a gradient instead of a flat colour |
iconRecolourable | False for multicolour artwork — flags, brand marks — which carries its own colours and ignores fillColor |
iconBody is injected into the page the renderer screenshots, so anything supplied through the API is reduced to drawing elements first: no script, foreignObject, use, image, event handlers or external url() references survive. Geometry is all an icon has ever needed to be.Live screenshots
An image layer can carry screenshot settings instead of a fixed picture. When it does, the renderer captures that page on every render — so a pricing template rendered next quarter shows next quarter’s prices, with nobody reopening the design.
{
"type": "image",
"imageUri": "https://cdn.pixbix.app/…/preview.png",
"screenshot": {
"enabled": true,
"url": "https://acme.com/pricing",
"viewportWidth": 1440,
"viewportHeight": 900,
"scale": 2,
"fullPage": false,
"blockCookieBanners": true,
"cacheTtlSec": 3600
}
}Binding a field to screenshot.url is the automation case: one template captures whichever page each caller names. imageUri stays on the layer as a preview, and is what the layer falls back to if a capture fails.
| Rule | Why |
|---|---|
| Requires the Screenshot feature on your plan | A live layer is a capture, and captures are a metered product |
| Costs one extra credit per layer, per render | Priced the same as any other capture. The estimate endpoint includes it. |
| At most 4 live layers per template | Each holds a browser slot while its page loads; this bounds what one template can do to the render fleet |
cacheTtlSec defaults to 3600 | A batch of renders of the same page captures it once. Set 0 for genuinely per-render pages. |
| A failed capture never fails the render | The layer keeps its preview and the render reports why in `warnings` |
screenshot.url still containing an unfilled {{placeholder}} is skipped rather than fetched, and is not charged for.Type
| Property | Meaning |
|---|---|
fontFamily, fontSize, fontWeight, fontStyle | Any Google font; only the cuts the family really ships are loaded |
textAlign, lineHeight, letterSpacing, wordSpacing | The usual metrics, in design pixels |
textTransform | uppercase, lowercase, capitalize |
textDecoration | underline, line-through, overline |
gradient | Paints a gradient and clips it to the glyphs |
textHighlight | A filled plate behind the text — the badge effect |
curve | Bend the baseline onto a circle of this radius. Positive arcs over the top, negative under the bottom. |
numberOfLines, ellipsizeMode | Clamp to N lines, with a tail ellipsis or a hard cut |
richText | Per-word styling — see below |
{{variable}} highlight colour does not apply to a curved run.Styling one word differently
Everything above sets the type for the WHOLE layer. richText overrides it for a slice of the string — one word in a brand colour, a number twice the size, a phrase in another face — without splitting the sentence across separate layers.
It is an array of runs. Each run carries its own textplus only the properties it changes; everything it does not name is inherited from the layer, so changing the layer’s font later still moves the styled words with it.
{
"type": "text",
"text": "Big Diwali Sale",
"fontFamily": "Poppins",
"fontSize": 48,
"textColor": "#111827",
"richText": [
{ "text": "Big " },
{ "text": "Diwali", "color": "#E11D48", "fontSize": 72, "fontWeight": "700" },
{ "text": " Sale" }
]
}| Property | Meaning |
|---|---|
text | The characters this run covers. Required. |
fontFamily, fontSize, fontWeight, fontStyle | Type, overriding the layer’s |
color | The glyph colour. Named color, not textColor — a run is a span. |
textDecoration, textTransform, letterSpacing | As on the layer, for these characters only |
backgroundColor | A highlighter behind the words, which survives a line wrap |
| Rule | What happens |
|---|---|
| The runs must spell `text` exactly | Concatenated, they have to equal the layer’s `text`. If they do not, the layer renders with its own styling and the runs are ignored — a wrong render is worse than an unstyled one. |
| A field that replaces the text drops the runs | The runs describe THOSE characters. Substituting a different string means the words they were cut from are gone, so the layer falls back to its own styling. |
| Variables still work inside a run | A run containing `{{price}}` keeps the variable highlight, composed with the run’s own colour. |
| Omit it for plain text | A single run with no overrides is the same as no `richText` at all. |
The canvas
backgroundColor is the floor, backgroundGradient paints over it, and backgroundImage covers both — sized by backgroundFit (cover, contain, fill or tile).
Importing a design#
POST /v1/templates/import turns a finished design from another tool into a template. What you get back depends entirely on what you send, and the difference is worth understanding before you pick an export format.
| You send | You get |
|---|---|
SVG | Text as real text layers — family, size, weight and colour as resolved by a browser — photos as image layers, and everything drawn flattened into one plate behind them. |
PNG / JPEG / WebP | One full-bleed image layer. A flat picture has no layers to recover, and nothing can invent them. |
HTML | The same treatment as SVG, for a fragment copied out of a design tool in the browser. |
PDF, AI, EPS | Rasterised at 150 DPI and imported as flat artwork — the first page only. Requires Ghostscript on the server; without it the request explains which export to use instead. |
PSD | Not supported. Export as PNG for the artwork, or as SVG from Illustrator if the text needs to stay editable. |
The import is laid out in real headless Chrome rather than parsed, so nested transforms, CSS classes, tspan runs and viewBox scaling all resolve the way they do when you look at the file. The geometry recorded is the geometry a browser computed.
import.fonts and import.warnings, so a design that came in with the wrong face tells you rather than waiting to be noticed.An import always lands as a private draft with no field schema. Which text a caller may set is a decision about your public contract, and guessing it from whatever strings happened to be in the artwork would make one for you. Declare your fields, give it a main category, then publish it — a draft cannot be rendered.
Status, type and the library#
A template answers three separate questions, and each has its own field. Conflating them is the most common source of a render that is refused for a reason that is not obvious, so they are worth reading once.
| Field | Question | Who decides |
|---|---|---|
status | Is it finished? draft or published | You |
visibility | Who is it for? private or public | You |
review.state | May it be listed in the library? | pixbix staff |
Only published templates render
status is the single gate on rendering. A draft is refused everywhere — by /v1/render, by /v1/generate, by every workflow step and by the dashboard — with TEMPLATE_NOT_PUBLISHED. Publishing is your own decision and takes effect immediately; nobody reviews it, because it only affects renders inside your own workspace.
Before a template can be published it must have a name, a main category, and:
- For image templates — at least one layer
- For video templates — a timeline with tracks and a duration
- No duplicate field keys (duplicates silently drop values at render time)
A public template is copied, never borrowed
You can only render a template your own workspace owns. Marking one public offers it to other accounts to copy — they take their own copy with POST /templates/:id/duplicateand render that. Naming somebody else’s template in a render returns TEMPLATE_NOT_OWNED with a message saying so.
The list and schema endpoints both report canRender so you never have to work this out from status and ownership yourself. When it is false, renderBlockedReason says what to do about it.
Getting into the library
Setting visibility: "public" puts the template in a staff review queue (review.state: "pending"). It appears in the shared library once approved and once it is also published. A public template additionally needs a thumbnail and at least one field — the two things somebody browsing needs in order to judge it.
Editing the design of a listed template sends the listing back for review, so the library cannot be edited into something else after approval. Your own renders keep working the whole time — status is untouched, and only the shopfront entry pauses. Going back to private delists it at once and needs no review.
Versioning#
Every change increments version. Each render records the version it used, so a template edited today does not change what a render from last month actually was.
Cache the field schema by version. When a template’s version changes, re-read it — a new required field will otherwise start failing your renders with a validation error.