JSON trees
The document model behind editable image ads and design templates. Schema version 2, a strict subset of Figma's Plugin API node shape.
A JSON tree is the structured document behind every editable image ad and design template. It describes every layer of the ad (frames, groups, shapes, images, text, icons) in the same shape Figma's Plugin API uses, so a tree goes into Figma and back without translation.
Mental model
Editable image ads and design templates both carry a tree in current_json_tree. The shape is the same; the difference is purpose. A design template is the layout blueprint, an editable image ad is the finished creative bound to a brand, product and audience.
The tree is a strict subset of Figma's Plugin API node shape. Property names, enums and units are Figma's, so Figma's documentation is the reference for what each property means:
- Node types
TransformandrelativeTransformPaint(solid, gradient and image fills)StyledTextSegmentandFontName
Anything that is not Figma lives under a sal key: the image URL on an image paint, and the icon reference on an icon vector.
The document
A tree is a document with three keys:
{
"schemaVersion": 2,
"colorProfile": "SRGB",
"root": { "type": "FRAME", "...": "the artboard" }
}| Key | Value |
|---|---|
schemaVersion | Always 2. Trees without it use the retired format; see Trees created before schema v2. |
colorProfile | Always "SRGB". Every color in the tree is sRGB, each channel 0..1. |
root | The artboard: a FRAME whose relativeTransform is the identity [[1, 0, 0], [0, 1, 0]] and whose width / height are the ad's pixel size. |
Node types
type | What it is | Figma reference |
|---|---|---|
FRAME | Container with its own fills, strokes, corner radii and optional clipping. The root is always a frame. | FrameNode |
GROUP | Container with no box of its own; its bounds are its children's bounds. | GroupNode |
RECTANGLE | Rectangle with per-corner radii. Most images are rectangles with an image fill. | RectangleNode |
ELLIPSE | Ellipse, optionally an arc or ring (arcData). | EllipseNode |
POLYGON / STAR | Regular polygon or star (pointCount, innerRadius). | PolygonNode, StarNode |
LINE | A line along its local x axis, width long, height 0. | LineNode |
VECTOR | SVG path data (vectorPaths). Icons are vectors. | VectorNode |
TEXT | Text with per-range styles (characters + styledTextSegments). | TextNode |
Every node has an id (unique within the tree) and a name. Common optional properties: visible, opacity, blendMode, effects (drop and inner shadows, layer and background blur), isMask / maskType. FRAME and GROUP carry children, painted in order (first child at the back).
Figma node types the tree does not include (components, instances, boolean operations) are flattened into the types above when a design comes in from Figma.
A complete example
A 1080x1080 ad with a product photo, a fade, a headline with one styled word, a pill-shaped button and a rotated star icon. This tree passes validation as written.
{
"schemaVersion": 2,
"colorProfile": "SRGB",
"root": {
"id": "root",
"name": "Ad 1080x1080",
"type": "FRAME",
"relativeTransform": [[1, 0, 0], [0, 1, 0]],
"width": 1080,
"height": 1080,
"fills": [{ "type": "SOLID", "color": { "r": 0.98, "g": 0.98, "b": 0.976 } }],
"strokes": [],
"strokeWeight": 0,
"strokeAlign": "INSIDE",
"topLeftRadius": 0,
"topRightRadius": 0,
"bottomRightRadius": 0,
"bottomLeftRadius": 0,
"clipsContent": true,
"children": [
{
"id": "product-photo",
"name": "Product photo",
"type": "RECTANGLE",
"relativeTransform": [[1, 0, 0], [0, 1, 0]],
"width": 1080,
"height": 720,
"fills": [
{
"type": "IMAGE",
"scaleMode": "FILL",
"sal": { "url": "https://cdn.example.com/leggings-lifestyle.jpg", "width": 2160, "height": 1440 }
}
],
"strokes": [],
"strokeWeight": 0,
"strokeAlign": "INSIDE",
"topLeftRadius": 0,
"topRightRadius": 0,
"bottomRightRadius": 0,
"bottomLeftRadius": 0
},
{
"id": "photo-fade",
"name": "Photo fade",
"type": "RECTANGLE",
"relativeTransform": [[1, 0, 0], [0, 1, 480]],
"width": 1080,
"height": 240,
"fills": [
{
"type": "GRADIENT_LINEAR",
"gradientTransform": [[0, 1, 0], [-1, 0, 1]],
"gradientStops": [
{ "position": 0, "color": { "r": 0.98, "g": 0.98, "b": 0.976, "a": 0 } },
{ "position": 1, "color": { "r": 0.98, "g": 0.98, "b": 0.976, "a": 1 } }
]
}
],
"strokes": [],
"strokeWeight": 0,
"strokeAlign": "INSIDE",
"topLeftRadius": 0,
"topRightRadius": 0,
"bottomRightRadius": 0,
"bottomLeftRadius": 0
},
{
"id": "headline",
"name": "Headline",
"type": "TEXT",
"relativeTransform": [[1, 0, 72], [0, 1, 768]],
"width": 936,
"height": 116,
"characters": "The GOAT of leggings",
"fills": [{ "type": "SOLID", "color": { "r": 0.11, "g": 0.11, "b": 0.11 } }],
"strokes": [],
"strokeWeight": 0,
"strokeAlign": "OUTSIDE",
"textAlignHorizontal": "LEFT",
"textAlignVertical": "TOP",
"textAutoResize": "HEIGHT",
"textTruncation": "DISABLED",
"maxLines": null,
"leadingTrim": "NONE",
"styledTextSegments": [
{
"start": 0,
"end": 4,
"fontName": { "family": "Inter", "style": "Medium" },
"fontSize": 96,
"fills": [{ "type": "SOLID", "color": { "r": 0.11, "g": 0.11, "b": 0.11 } }],
"textCase": "ORIGINAL",
"textDecoration": "NONE",
"lineHeight": { "unit": "PERCENT", "value": 120 },
"letterSpacing": { "unit": "PERCENT", "value": -2 },
"paragraphSpacing": 0,
"paragraphIndent": 0,
"listOptions": { "type": "NONE" },
"listSpacing": 0,
"indentation": 0
},
{
"start": 4,
"end": 8,
"fontName": { "family": "Inter", "style": "Extra Bold Italic" },
"fontSize": 96,
"fills": [{ "type": "SOLID", "color": { "r": 0.102, "g": 0.102, "b": 0.18 } }],
"textCase": "ORIGINAL",
"textDecoration": "NONE",
"lineHeight": { "unit": "PERCENT", "value": 120 },
"letterSpacing": { "unit": "PERCENT", "value": -2 },
"paragraphSpacing": 0,
"paragraphIndent": 0,
"listOptions": { "type": "NONE" },
"listSpacing": 0,
"indentation": 0
},
{
"start": 8,
"end": 20,
"fontName": { "family": "Inter", "style": "Medium" },
"fontSize": 96,
"fills": [{ "type": "SOLID", "color": { "r": 0.11, "g": 0.11, "b": 0.11 } }],
"textCase": "ORIGINAL",
"textDecoration": "NONE",
"lineHeight": { "unit": "PERCENT", "value": 120 },
"letterSpacing": { "unit": "PERCENT", "value": -2 },
"paragraphSpacing": 0,
"paragraphIndent": 0,
"listOptions": { "type": "NONE" },
"listSpacing": 0,
"indentation": 0
}
]
},
{
"id": "cta-button",
"name": "CTA button",
"type": "RECTANGLE",
"relativeTransform": [[1, 0, 72], [0, 1, 920]],
"width": 320,
"height": 88,
"fills": [{ "type": "SOLID", "color": { "r": 0.102, "g": 0.102, "b": 0.18 }, "opacity": 0.9 }],
"strokes": [],
"strokeWeight": 0,
"strokeAlign": "INSIDE",
"topLeftRadius": 44,
"topRightRadius": 44,
"bottomRightRadius": 44,
"bottomLeftRadius": 44
},
{
"id": "badge-icon",
"name": "Star icon",
"type": "VECTOR",
"relativeTransform": [[0.965926, -0.258819, 960], [0.258819, 0.965926, 64]],
"width": 48,
"height": 48,
"vectorPaths": [
{
"windingRule": "NONZERO",
"data": "M 24 2 L 30.5 17 L 46 18.5 L 34 29 L 37.5 45 L 24 36.5 L 10.5 45 L 14 29 L 2 18.5 L 17.5 17 Z"
}
],
"fills": [{ "type": "SOLID", "color": { "r": 0.961, "g": 0.62, "b": 0.043 } }],
"strokes": [],
"strokeWeight": 0,
"strokeAlign": "CENTER",
"sal": { "icon": { "id": "star", "weight": "fill" } }
}
]
}
}Geometry
Every node except GROUP stores its position, rotation and flip as a relativeTransform plus its unrotated size as width and height. There are no x, y or rotation fields.
relativeTransform is Figma's 2x3 affine matrix, rows [[m00, m01, m02], [m10, m11, m12]]. A point (px, py) in the node's own box maps to (m00·px + m01·py + m02, m10·px + m11·py + m12) in its container. Read the familiar values from it:
x = m02andy = m12: where the node's top-left corner sits.rotation = atan2(-m10, m00)in degrees. Positive is counter-clockwise on screen, and a node rotates about its top-left corner. The star icon above is atx = 960,y = 64, rotation-15, which is 15 degrees clockwise.- A negative determinant (
m00·m11 - m01·m10) means the node is flipped.
function nodePosition(node) {
const [[m00, , m02], [m10, , m12]] = node.relativeTransform;
return {
x: m02,
y: m12,
rotation: (Math.atan2(-m10, m00) * 180) / Math.PI,
};
}
/** Moves a node without changing its rotation or flip. */
function moveNode(node, x, y) {
node.relativeTransform[0][2] = x;
node.relativeTransform[1][2] = y;
}The transform is relative to the node's container: the nearest FRAME ancestor. Groups have no coordinate space of their own, so a group's children are positioned in the group's container, exactly as in Figma. A GROUP stores no relativeTransform, width or height; to move a group, move each of its children.
Auto-layout properties (layoutMode, itemSpacing, padding, layoutSizingHorizontal and the rest) may appear on frames and their children. They describe how a design was built; the renderer draws the stored transforms and sizes, and never re-runs auto-layout.
Fills and colors
fills (and strokes) are lists of paints, painted bottom to top. Three paint types are supported.
Solid. color is RGB with channels 0..1. Alpha is the paint's opacity, not part of the color, as in Figma's Plugin API.
{ "type": "SOLID", "color": { "r": 0.102, "g": 0.102, "b": 0.18 }, "opacity": 0.9 }To convert a hex color: r = parseInt("1a", 16) / 255, and so on for g and b.
Gradients. GRADIENT_LINEAR, GRADIENT_RADIAL, GRADIENT_ANGULAR and GRADIENT_DIAMOND take gradientStops (each color is RGBA) and a gradientTransform. The transform maps the node's unit box (0..1 on both axes) into gradient space, where a linear gradient runs from (0, 0.5) to (1, 0.5). The identity [[1, 0, 0], [0, 1, 0]] runs left to right; [[0, 1, 0], [-1, 0, 1]] runs top to bottom.
Images. An image is an IMAGE paint on any shape, usually a RECTANGLE. sal.url is a public https URL of the image; sal.width / sal.height are its pixel size when known. scaleMode is Figma's: FILL, FIT, CROP (with imageTransform) or TILE (with scalingFactor).
{
"type": "IMAGE",
"scaleMode": "FILL",
"sal": { "url": "https://cdn.example.com/leggings-lifestyle.jpg" }
}Trees never contain image bytes or base64. imageHash only means something inside one Figma file; the renderer always reads sal.url. Image filters are kept for round trips with Figma but are not drawn yet.
Corner radii
RECTANGLE and FRAME have four radius fields, each in pixels: topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius. Optional cornerSmoothing (0..1, 0.6 is the iOS-style squircle) matches Figma's slider. POLYGON, STAR and VECTOR take a single optional cornerRadius.
Text
A TEXT node stores its string in characters and its styles in styledTextSegments, the shape Figma's getStyledTextSegments returns. There are no node-level font fields: every style lives on a segment.
- Segments cover the text exactly: the first starts at
0, each starts where the previous one ended, and the last ends atcharacters.length(JavaScript string length, in UTF-16 code units). No gaps, no overlaps. Empty text keeps one segment withstart: 0, end: 0. - Each segment has
fontName,fontSize,fills,lineHeight,letterSpacing,textCase,textDecoration, paragraph and list settings. Segmentfillsare what renders; the node's ownfillsis a fallback. fontNameis{ family, style }.styleis the font's real style name, such as"Regular","Semi Bold"or"Extra Bold Italic". There is nofontWeightand no italic flag: weight and slant both come from the style name. Use a family and style from Supported fonts; any other font renders in Inter (see Fonts we do not ship). Variable fonts also acceptvariationSettings, such as{ "wght": 550 }.- Unlike Figma's own
StyledTextSegment, a segment has nocharacterskey;startandendsay which characters it covers. - Node-level layout:
textAlignHorizontal,textAlignVertical,textAutoResize,textTruncation,maxLinesandleadingTrim.
When you replace the text, rebuild the segments so they still cover it. The simplest way is to keep the first segment's style for the whole string:
function setText(node, text) {
const [style] = node.styledTextSegments;
node.characters = text;
node.styledTextSegments = [{ ...style, start: 0, end: text.length }];
}To keep a styled word, split the new string into ranges and give each range a copy of the segment style you want, with its own start and end.
Icons
Icons are VECTOR nodes. vectorPaths holds the SVG path data, drawn in the node's own box (0..width by 0..height), so resizing an icon means scaling its path data too. sal.icon records which icon it is (id) and its weight (thin, light, regular, bold or fill).
sal.icon is metadata only. The renderer draws vectorPaths, so changing sal.icon.id alone does not change the picture: send the new icon's paths as well.
Validation
PATCH endpoints validate the whole tree before any render work. The schema is strict:
- Unknown keys are rejected, at every level. A v1 field such as
fontFamily,imageFillUrl,xorviewBoxfails withUnrecognized key. - Required fields must be present, enums must use Figma's spelling, and numbers must be finite.
rootmust be aFRAMEwith an identityrelativeTransform, node ids must be unique, and text segments must cover their characters exactly.
A tree that fails returns 400 VALIDATION_ERROR with one details entry per problem, each naming the exact path:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "current_json_tree is not a valid JSON tree (schema version 2): 1 issue(s).",
"details": [
{
"field": "current_json_tree.root.children[2].fills[0].color.r",
"message": "Invalid input: expected number, received string"
}
]
},
"meta": { "request_id": "req_abc123", "timestamp": "2026-10-25T14:30:00.000Z" }
}PATCH /v1/image-ads/:id rejects these before saving anything:
| Code | Status | When |
|---|---|---|
VALIDATION_ERROR | 400 | The tree is invalid or uses the retired format. details list each path. |
INVALID_IMAGE_URL | 400 | An IMAGE paint's sal.url is not an absolute http(s) URL (blob:, data:, a bare name). details list each paint path. |
REVISION_CONFLICT | 409 | base_revision is not the ad's current tree_revision. details.current_revision is the one to rebase on. |
Once saved, the tree is rendered in the background. A render failure does not undo the save: the ad keeps its previous image_url and reports render_status: "failed" with a render_error:
render_error.code | When |
|---|---|
IMAGE_UNAVAILABLE | An image could not be downloaded. details list each URL and its paint path (...fills[0].sal.url). |
RENDER_FAILED | Any other render failure. Save again to retry; if it keeps failing, contact support. |
PATCH /v1/design-templates/:id runs the same validation (the 400s) but does not render. Figma sync renders before saving, so each item gets VALIDATION_ERROR, INVALID_IMAGE_URL, IMAGE_UNAVAILABLE or RENDER_FAILED on its own result and the other items still sync.
Trees created before schema v2
Ads and templates created before schema v2 keep the tree they were saved with. Reads (GET on the ad or template, list endpoints and versions) return those trees as stored, in the retired format: no schemaVersion, and v1 fields such as x, fontFamily and imageFillUrl. The one addition on read is icons: VECTOR nodes with an iconId get their vectorPaths and viewBox filled in.
Writes only accept schema v2. Sending a retired tree to PATCH returns 400 VALIDATION_ERROR with the message "This JSON tree uses the retired schema (version 1). Regenerate it with the current pipeline." There is no automatic conversion: to edit an older ad, generate a new editable ad. Check the version before you edit:
function isSchemaV2(tree) {
return tree?.schemaVersion === 2;
}
if (!isSchemaV2(ad.current_json_tree)) {
throw new Error(`Ad ${ad.id} uses the retired JSON tree format. Generate it again to edit it.`);
}Mutating a tree
Use PATCH /v1/image-ads/:id with the full updated document in current_json_tree. Always send the entire tree, not a diff. Editable ads create their tree after generation completes, so wait for status: "completed" before reading or editing it. See Edit a JSON tree and re-render. For a targeted change you can skip the full tree and send an instruction or a design script to POST /v1/image-ads/:id/edits instead.
To swap an image, upload it with POST /v1/images (multipart) and put the returned src in the paint's sal.url, or use any public https URL of your own.
Supported fonts
Text renders in the fonts below, all from Google Fonts. Any other font renders in Inter (see Fonts we do not ship).
- Family is the exact
fontName.family. - Styles are the exact
fontName.stylevalues. "each + Italic" means every listed weight also ships as"<weight> Italic", and the italic of Regular is"Italic". Lookups ignore case and spaces, and"Regular Italic"matches"Italic". *marks variable fonts, which also acceptfontName.variationSettings.
| Family | Category | Styles |
|---|---|---|
| Abril Fatface | Display | Regular |
| Alfa Slab One | Display | Regular |
| Amatic SC | Handwriting | Regular, Bold |
| Anton | Display | Regular |
| Archivo Black | Sans serif | Regular |
| Arvo | Serif | Regular, Bold, each + Italic |
| Audiowide | Display | Regular |
| Bangers | Display | Regular |
| Barlow | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Bebas Neue | Display | Regular |
| Bitter * | Serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Black Ops One | Display | Regular |
| Bodoni Moda * | Serif | Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Bree Serif | Serif | Regular |
| Bungee | Display | Regular |
| Caveat * | Handwriting | Regular, Medium, Semi Bold, Bold |
| Changa One | Display | Regular + Italic |
| Cinzel * | Serif | Regular, Medium, Semi Bold, Bold, Extra Bold, Black |
| Cinzel Decorative | Display | Regular, Bold, Black |
| Comfortaa * | Display | Light, Regular, Medium, Semi Bold, Bold |
| Concert One | Display | Regular |
| Cormorant Garamond * | Serif | Light, Regular, Medium, Semi Bold, Bold, each + Italic |
| Crimson Pro * | Serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Crimson Text | Serif | Regular, Semi Bold, Bold, each + Italic |
| Dancing Script * | Handwriting | Regular, Medium, Semi Bold, Bold |
| DM Mono | Monospace | Light, Regular, Medium, each + Italic |
| DM Sans * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| DM Serif Display | Serif | Regular + Italic |
| Domine * | Serif | Regular, Medium, Semi Bold, Bold |
| EB Garamond * | Serif | Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic |
| Figtree * | Sans serif | Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Fira Code * | Monospace | Light, Regular, Retina, Medium, Semi Bold, Bold |
| Fira Sans | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Fraunces * | Serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Fredoka * | Sans serif | Light, Regular, Medium, Semi Bold, Bold |
| GFS Didot | Serif | Regular |
| Gilda Display | Serif | Regular |
| Great Vibes | Handwriting | Regular |
| IBM Plex Mono | Monospace | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic |
| IBM Plex Sans * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic |
| IBM Plex Serif | Serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic |
| Indie Flower | Handwriting | Regular |
| Instrument Serif | Serif | Regular + Italic |
| Inter * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| JetBrains Mono * | Monospace | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic |
| Josefin Sans * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic |
| Jost * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Karla * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic |
| Kaushan Script | Handwriting | Regular |
| Lato | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| League Spartan * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black |
| Libre Baskerville * | Serif | Regular, Medium, Semi Bold, Bold, each + Italic |
| Libre Caslon Text * | Serif | Regular, Medium, Semi Bold, Bold, each + Italic |
| Lilita One | Display | Regular |
| Lobster | Display | Regular |
| Lobster Two | Display | Regular, Bold, each + Italic |
| Lora * | Serif | Regular, Medium, Semi Bold, Bold, each + Italic |
| Luckiest Guy | Display | Regular |
| Manrope * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold |
| Merriweather * | Serif | Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Monoton | Display | Regular |
| Montserrat * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Mulish * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, Extra Black, each + Italic |
| Newsreader * | Serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic |
| Noto Sans * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Noto Serif * | Serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Nunito * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Nunito Sans * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Open Sans * | Sans serif | Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic. Condensed: Light, Regular, Semi Bold, Bold, Extra Bold, each + Italic |
| Oswald * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold |
| Outfit * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black |
| Pacifico | Handwriting | Regular |
| Passion One | Display | Regular, Bold, Black |
| Patua One | Display | Regular |
| Permanent Marker | Handwriting | Regular |
| Playfair Display * | Serif | Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Plus Jakarta Sans * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic |
| Poppins | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Prata | Serif | Regular |
| Press Start 2P | Display | Regular |
| PT Sans | Sans serif | Regular, Bold, each + Italic |
| PT Serif | Serif | Regular, Bold, each + Italic |
| Quicksand * | Sans serif | Light, Regular, Medium, Semi Bold, Bold |
| Raleway * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Righteous | Display | Regular |
| Roboto * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic. Condensed: Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Roboto Condensed * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Roboto Mono * | Monospace | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic |
| Roboto Slab * | Serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black |
| Rubik * | Sans serif | Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Sacramento | Handwriting | Regular |
| Satisfy | Handwriting | Regular |
| Shadows Into Light | Handwriting | Regular |
| Source Code Pro * | Monospace | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Source Sans 3 * | Sans serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Source Serif 4 * | Serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Space Grotesk * | Sans serif | Light, Regular, Medium, Semi Bold, Bold |
| Space Mono | Monospace | Regular, Bold, each + Italic |
| Special Elite | Display | Regular |
| Spectral | Serif | Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic |
| Staatliches | Display | Regular |
| Titan One | Display | Regular |
| Ubuntu | Sans serif | Light, Regular, Medium, Bold, each + Italic |
| Vollkorn * | Serif | Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Work Sans * | Sans serif | Thin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic |
| Yeseva One | Display | Regular |
| Zilla Slab | Serif | Light, Regular, Medium, Semi Bold, Bold, each + Italic |
Emoji in characters render with Noto Color Emoji automatically; it is not a family you can name.
Not available (they render in Inter): Impact, and the old Google names Source Sans Pro, Source Serif Pro and Fredoka One. Use Source Sans 3, Source Serif 4 and Fredoka instead.
Fonts we do not ship
A tree can name any font, for example one a designer has installed locally in Figma. When the family is not in the table above, or the family is there but not that style, the text renders in Inter at the closest style instead of failing:
- Weight comes from the style name (
Thin100 …Black900;Bookis Regular,Heavyis Black,SemiBoldandSemi Boldboth work), or fromvariationSettings.wghtwhen set. ItalicorObliquein the style name renders as Inter's italic of that weight.- The tree keeps the original
fontName, so the design still opens with its own font in Figma. Layout (auto width, line breaks) uses Inter too, so the render is consistent.
To render the intended font, use a family and style from the table above.
Endpoints
JSON trees are part of the Image ads and Design templates resources. There are no standalone JSON tree endpoints.
Pitfalls
- Trees are large for typical ads (a few KB to around 100 KB). Don't log the full tree in production.
- Node ids are stable across versions of the same ad. Use them to track "the headline node" reliably.
- The schema is strict: don't add your own keys to nodes or paints. Keep your own metadata outside the tree, keyed by node id.
- After changing
characters, updatestyledTextSegmentsso they cover the new length, or thePATCHfails validation. - Moving a
GROUPmeans moving its children; groups have no transform of their own.
Related
Prompt for your agent
Read https://www.staticadslab.com/docs/resources/json-trees.mdx and write a helper findTextNodeByCharacters(tree, query) that walks tree.root and returns the TEXT node whose characters include the query string, and a helper setText(node, text) that replaces the characters and rebuilds styledTextSegments so they cover the new text.Editable image ads
Every image ad is editable. PNG plus a structured JSON tree you can change by instruction, script or PATCH; the PNG re-renders in the background. Versioned and Figma-syncable.
Design scripts
The JavaScript API for editing an ad's layers, modeled on Figma's Plugin API. One script is one atomic edit.