Static Ads Lab
Concepts

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.

Every image ad is editable. In addition to the Meta-ready PNG, you get a JSON tree — a structured document representing every visual element. Change it with a plain-English instruction or your own design script (POST /v1/image-ads/:id/edits), or save a whole new tree with PATCH. The API saves the change immediately and re-renders the PNG in the background.

Ways to edit

  • Design edits: one request with an instruction (our AI Design Agent, model cost + 30%) or a script (your own agent, free). Atomic and synchronous.
  • Full tree save: for a custom ad editor, or programmatic variants (e.g., 50 ads with the same layout but different headlines).
  • Figma: round-trip an ad through Figma with the plugin.

Lifecycle

stateDiagram-v2
  [*] --> processing: POST /v1/image-ads (202)
  processing --> completed: image_url + current_json_tree ready
  processing --> failed
  completed --> completed: PATCH, design edit or Figma sync
  completed --> [*]

A PATCH, an applied design edit or a Figma sync validates and saves the new tree, increments tree_revision, and returns right away with render_status: "pending". The image is then rendered in the background at 2× (a 1080-wide canvas exports 2160 wide). The image matches the tree once rendered_tree_revision equals tree_revision. A failed render sets render_status: "failed" and render_error, and keeps the saved tree and the previous image. The ad stays completed throughout. The previous tree is kept as a version (iav_…) at most once every 10 minutes.

FieldMeaning
tree_revisionIncrements on every saved tree (PATCH, design edit, Figma sync). Send it back as base_revision.
rendered_tree_revisionThe revision image_url was rendered from.
render_statusidle, pending, rendering, or failed.
render_error{ code, message, details } of the last failed render, or null.

To wait for a render without downloading the tree each time, poll GET /v1/image-ads/:id/render-status. It returns { id, tree_revision, rendered_tree_revision, render_status, render_error, image_url, thumbnail_url, updated_at }.

Endpoints

Beyond the core Image ads endpoints, these work on an ad's tree:

MethodPathPurpose
POST/v1/image-ads/:id/editsEdit with an instruction or script
GET/v1/image-ads/:id/editsList design edits
GET/v1/image-ads/:id/outlineLayer outline
GET/v1/image-ads/:id/render-statusRender status
PATCH/v1/image-ads/:idUpdate the JSON tree
POST/v1/image-ads/:id/assetsUpload an image for the tree
GET/v1/image-ads/:id/versionsList versions
GET/v1/image-ads/:id/versions/:versionIdGet a specific version
POST/v1/image-ads/syncSync from Figma

Common patterns

Generate, then change a headline

const create = await fetch("https://api.staticadslab.com/v1/image-ads", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    reference_ad_url: "https://cdn.example.com/reference-ad.png",
    brand_id: "brand_...",
    product_id: "prod_...",
    audience_id: "aud_...",
  }),
});
const { data: created } = await create.json();
const ad = await waitForCompletion(created.id);

const tree = ad.current_json_tree; // { schemaVersion: 2, colorProfile: "SRGB", root }
const headline = findTextNodeByCharacters(tree.root, "Limited time");

// Replace the text and keep one style covering all of it.
const [style] = headline.styledTextSegments;
headline.characters = "Black Friday: 40% off";
headline.styledTextSegments = [{ ...style, start: 0, end: headline.characters.length }];

const response = await fetch(`https://api.staticadslab.com/v1/image-ads/${ad.id}`, {
  method: "PATCH",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ current_json_tree: tree, base_revision: ad.tree_revision }),
});
const body = await response.json();
if (!response.ok) {
  // 400 VALIDATION_ERROR / INVALID_IMAGE_URL, or 409 REVISION_CONFLICT.
  // error.details names each offending path (or the current revision). Nothing was saved.
  throw new Error(`${body.error.code}: ${body.error.message}`);
}

// Saved. Wait for the background render of this revision.
const rendered = await waitForRender(ad.id, body.data.tree_revision);
console.log(rendered.image_url); // the new render

findTextNodeByCharacters and waitForRender are in the edit and re-render guide.

List versions

const response = await fetch(
  `https://api.staticadslab.com/v1/image-ads/${adId}/versions`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const { data } = await response.json();
data.forEach((v) => console.log(v.version_number, v.image_url));

Pitfalls

  • current_json_tree is null until the ad completes. Check editor_supported before editing an older ad.
  • The tree must be a schema-v2 document. Ads created before schema v2 are returned in the retired format and cannot be patched; generate them again to edit them.
  • Use fonts we ship (Supported fonts). Any other font renders in Inter at the closest style; the tree keeps its fontName.
  • Images are IMAGE paints whose sal.url is an absolute public http(s) URL; anything else returns 400 INVALID_IMAGE_URL. Upload local files with POST /v1/image-ads/:id/assets (about 4 MB at most). The API does not accept base64 image data in JSON.
  • image_url lags the tree until the background render finishes. Wait for rendered_tree_revision == tree_revision before downloading.
  • Versions are immutable. Once written, a version's json_tree and image_url cannot be changed.

Prompt for your agent

Read https://www.staticadslab.com/docs/resources/editable-image-ads.mdx and write a function patchHeadline(adId, newHeadline) that updates an ad's headline node, rebuilds its styledTextSegments to cover the new text, saves it with PATCH (sending base_revision), then polls GET until rendered_tree_revision equals tree_revision and returns the new image_url.