Static Ads Lab
Concepts

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:

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" }
}
KeyValue
schemaVersionAlways 2. Trees without it use the retired format; see Trees created before schema v2.
colorProfileAlways "SRGB". Every color in the tree is sRGB, each channel 0..1.
rootThe 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

typeWhat it isFigma reference
FRAMEContainer with its own fills, strokes, corner radii and optional clipping. The root is always a frame.FrameNode
GROUPContainer with no box of its own; its bounds are its children's bounds.GroupNode
RECTANGLERectangle with per-corner radii. Most images are rectangles with an image fill.RectangleNode
ELLIPSEEllipse, optionally an arc or ring (arcData).EllipseNode
POLYGON / STARRegular polygon or star (pointCount, innerRadius).PolygonNode, StarNode
LINEA line along its local x axis, width long, height 0.LineNode
VECTORSVG path data (vectorPaths). Icons are vectors.VectorNode
TEXTText 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 = m02 and y = 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 at x = 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 at characters.length (JavaScript string length, in UTF-16 code units). No gaps, no overlaps. Empty text keeps one segment with start: 0, end: 0.
  • Each segment has fontName, fontSize, fills, lineHeight, letterSpacing, textCase, textDecoration, paragraph and list settings. Segment fills are what renders; the node's own fills is a fallback.
  • fontName is { family, style }. style is the font's real style name, such as "Regular", "Semi Bold" or "Extra Bold Italic". There is no fontWeight and 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 accept variationSettings, such as { "wght": 550 }.
  • Unlike Figma's own StyledTextSegment, a segment has no characters key; start and end say which characters it covers.
  • Node-level layout: textAlignHorizontal, textAlignVertical, textAutoResize, textTruncation, maxLines and leadingTrim.

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, x or viewBox fails with Unrecognized key.
  • Required fields must be present, enums must use Figma's spelling, and numbers must be finite.
  • root must be a FRAME with an identity relativeTransform, 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:

CodeStatusWhen
VALIDATION_ERROR400The tree is invalid or uses the retired format. details list each path.
INVALID_IMAGE_URL400An IMAGE paint's sal.url is not an absolute http(s) URL (blob:, data:, a bare name). details list each paint path.
REVISION_CONFLICT409base_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.codeWhen
IMAGE_UNAVAILABLEAn image could not be downloaded. details list each URL and its paint path (...fills[0].sal.url).
RENDER_FAILEDAny 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.style values. "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 accept fontName.variationSettings.
FamilyCategoryStyles
Abril FatfaceDisplayRegular
Alfa Slab OneDisplayRegular
Amatic SCHandwritingRegular, Bold
AntonDisplayRegular
Archivo BlackSans serifRegular
ArvoSerifRegular, Bold, each + Italic
AudiowideDisplayRegular
BangersDisplayRegular
BarlowSans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Bebas NeueDisplayRegular
Bitter *SerifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Black Ops OneDisplayRegular
Bodoni Moda *SerifRegular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Bree SerifSerifRegular
BungeeDisplayRegular
Caveat *HandwritingRegular, Medium, Semi Bold, Bold
Changa OneDisplayRegular + Italic
Cinzel *SerifRegular, Medium, Semi Bold, Bold, Extra Bold, Black
Cinzel DecorativeDisplayRegular, Bold, Black
Comfortaa *DisplayLight, Regular, Medium, Semi Bold, Bold
Concert OneDisplayRegular
Cormorant Garamond *SerifLight, Regular, Medium, Semi Bold, Bold, each + Italic
Crimson Pro *SerifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Crimson TextSerifRegular, Semi Bold, Bold, each + Italic
Dancing Script *HandwritingRegular, Medium, Semi Bold, Bold
DM MonoMonospaceLight, Regular, Medium, each + Italic
DM Sans *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
DM Serif DisplaySerifRegular + Italic
Domine *SerifRegular, Medium, Semi Bold, Bold
EB Garamond *SerifRegular, Medium, Semi Bold, Bold, Extra Bold, each + Italic
Figtree *Sans serifLight, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Fira Code *MonospaceLight, Regular, Retina, Medium, Semi Bold, Bold
Fira SansSans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Fraunces *SerifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Fredoka *Sans serifLight, Regular, Medium, Semi Bold, Bold
GFS DidotSerifRegular
Gilda DisplaySerifRegular
Great VibesHandwritingRegular
IBM Plex MonoMonospaceThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic
IBM Plex Sans *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic
IBM Plex SerifSerifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic
Indie FlowerHandwritingRegular
Instrument SerifSerifRegular + Italic
Inter *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
JetBrains Mono *MonospaceThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic
Josefin Sans *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic
Jost *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Karla *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic
Kaushan ScriptHandwritingRegular
LatoSans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
League Spartan *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black
Libre Baskerville *SerifRegular, Medium, Semi Bold, Bold, each + Italic
Libre Caslon Text *SerifRegular, Medium, Semi Bold, Bold, each + Italic
Lilita OneDisplayRegular
LobsterDisplayRegular
Lobster TwoDisplayRegular, Bold, each + Italic
Lora *SerifRegular, Medium, Semi Bold, Bold, each + Italic
Luckiest GuyDisplayRegular
Manrope *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold
Merriweather *SerifLight, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
MonotonDisplayRegular
Montserrat *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Mulish *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, Extra Black, each + Italic
Newsreader *SerifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic
Noto Sans *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Noto Serif *SerifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Nunito *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Nunito Sans *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Open Sans *Sans serifLight, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic. Condensed: Light, Regular, Semi Bold, Bold, Extra Bold, each + Italic
Oswald *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold
Outfit *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black
PacificoHandwritingRegular
Passion OneDisplayRegular, Bold, Black
Patua OneDisplayRegular
Permanent MarkerHandwritingRegular
Playfair Display *SerifRegular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Plus Jakarta Sans *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic
PoppinsSans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
PrataSerifRegular
Press Start 2PDisplayRegular
PT SansSans serifRegular, Bold, each + Italic
PT SerifSerifRegular, Bold, each + Italic
Quicksand *Sans serifLight, Regular, Medium, Semi Bold, Bold
Raleway *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
RighteousDisplayRegular
Roboto *Sans serifThin, 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 serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Roboto Mono *MonospaceThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, each + Italic
Roboto Slab *SerifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black
Rubik *Sans serifLight, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
SacramentoHandwritingRegular
SatisfyHandwritingRegular
Shadows Into LightHandwritingRegular
Source Code Pro *MonospaceExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Source Sans 3 *Sans serifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Source Serif 4 *SerifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Space Grotesk *Sans serifLight, Regular, Medium, Semi Bold, Bold
Space MonoMonospaceRegular, Bold, each + Italic
Special EliteDisplayRegular
SpectralSerifExtra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, each + Italic
StaatlichesDisplayRegular
Titan OneDisplayRegular
UbuntuSans serifLight, Regular, Medium, Bold, each + Italic
Vollkorn *SerifRegular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Work Sans *Sans serifThin, Extra Light, Light, Regular, Medium, Semi Bold, Bold, Extra Bold, Black, each + Italic
Yeseva OneDisplayRegular
Zilla SlabSerifLight, 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 (Thin 100 … Black 900; Book is Regular, Heavy is Black, SemiBold and Semi Bold both work), or from variationSettings.wght when set.
  • Italic or Oblique in 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, update styledTextSegments so they cover the new length, or the PATCH fails validation.
  • Moving a GROUP means moving its children; groups have no transform of their own.

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.