Static Ads Lab
Guides

Edit ads with instructions or scripts

Change a finished ad's layers in one request, with a plain-English instruction for our AI Design Agent or a design script your own agent writes. Atomic, synchronous, and re-rendered in the background.

Every image ad is a layered design. POST /v1/image-ads/:id/edits changes those layers in one atomic step and returns in a few seconds. Send one of two things:

  • instruction: a plain-English request ("Make the headline black"). Our AI Design Agent reads the ad's layers, writes a design script and runs it. Billed at model cost + 30%, rounded up to the cent, per request.
  • script: a design script your own agent wrote, in our Figma-Plugin-API-style design script API. Free.

An edit either applies completely or not at all. A failed edit changes nothing and is not charged.

When to use this

  • Recoloring, resizing, moving or rewording layers on a finished ad.
  • Letting your own LLM agent make design changes with a precise, scriptable API.
  • Small, targeted changes. To change the tree wholesale (for example from your own editor), use PATCH instead.

End-to-end flow

sequenceDiagram
  participant App as Your app
  participant API as Static Ads Lab API
  App->>API: GET /v1/image-ads/:id/outline
  API-->>App: layers (ids, names, types, text, boxes)
  App->>API: POST /v1/image-ads/:id/edits { instruction } or { script }
  API-->>App: 200 { status: applied, revision_after, render_status }
  Note right of API: image re-renders in the background
  App->>API: GET /v1/image-ads/:id/render-status (poll)
  API-->>App: rendered_tree_revision == tree_revision, image_url

Edit with an instruction

const API = "https://api.staticadslab.com";
const headers = {
  "X-API-Key": process.env.STATICADS_API_KEY,
  "Content-Type": "application/json",
};

const response = await fetch(`${API}/v1/image-ads/${adId}/edits`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": `edit:${adId}:headline-black` },
  body: JSON.stringify({ instruction: "Make the headline black" }),
});
const { data: edit } = await response.json();

if (edit.status === "failed") {
  // Nothing was applied and nothing was charged.
  console.warn(edit.failure.code, edit.failure.message);
} else {
  console.log(edit.summary, edit.changed_layer_ids, edit.cost_cents);
}

Put every change for one ad in one instruction ("Make the headline black and move the logo to the top left"). The agent writes one script for the whole request.

Edit with your own script

Fetch the outline to see the layers, then send a script. Scripts run in a sandbox against the ad's current design. See Design scripts for the full API.

const outline = await fetch(`${API}/v1/image-ads/${adId}/outline`, { headers })
  .then((r) => r.json());
// outline.data describes each layer: id, name, type, text and box.

const script = `
  const headline = doc.findOne({ name: "Headline" });
  headline.fills = ["#000000"];
  return headline.bounds;
`;

const response = await fetch(`${API}/v1/image-ads/${adId}/edits`, {
  method: "POST",
  headers,
  body: JSON.stringify({ script }),
});
const { data: edit } = await response.json();

Script edits are free. Your agent can read the outline, write a script, check warnings, and try again in a loop without paying per attempt.

Request body

FieldTypeDescription
instructionstringA plain-English change. Send this or script, not both.
scriptstringA design script. Send this or instruction.
base_revisionnumberOptional. The tree_revision you based the edit on. If the ad has been saved since, you get 409 REVISION_CONFLICT and nothing is applied.
scope_layer_idsstring[]Optional. Layer ids the edit may change. An edit that changes anything else fails and applies nothing.

The endpoint needs the image_ads:write scope and accepts an Idempotency-Key header.

Response

200 OK, whether the edit applied or failed:

{
  "data": {
    "id": "ied_...",
    "image_ad_id": "ia_...",
    "status": "applied",
    "mode": "instruction",
    "summary": "Changed the headline color to black.",
    "changed_layer_ids": ["12:4"],
    "warnings": [
      {
        "rule": "low_contrast",
        "layer_ids": ["12:4", "12:1"],
        "message": "Text \"Headline\" (#000000) on \"Background\" (#1A1A1A) has 1.2:1 contrast; aim for at least 4.5:1."
      }
    ],
    "revision_before": 7,
    "revision_after": 8,
    "retry_safe": true,
    "cost_cents": 1,
    "render_status": "pending",
    "failure": null,
    "created_at": "2026-11-01T18:04:12.000Z"
  },
  "meta": { "request_id": "req_...", "timestamp": "..." }
}
FieldMeaning
statusapplied or failed. A failed edit applied nothing.
modeinstruction or script
summaryWhat changed, in one sentence
changed_layer_idsLayers created, removed or changed
warningsDesign problems the edit introduced, such as text off the canvas, overflowing its box, overlapping other text, too small to read on a phone, low contrast, or sitting where Stories and Reels UI covers it. The edit still applied.
revision_before / revision_afterThe ad's tree_revision before and after
retry_safeAlways true: edits are atomic, so a failed edit can be retried as is
cost_centsWhat this edit cost. 0 for scripts and for failed edits.
render_statusThe ad's render state after the edit, usually pending
failure{ code, message } when status is failed, otherwise null

Common failure codes include script_error (the script threw or timed out), invalid_design (the result is not a valid design), no_change (nothing changed) and out_of_scope (it changed layers outside scope_layer_ids). Read failure.message for the details.

Wait for the new image

The design is saved when the edit returns. The image re-renders in the background, the same way it does after a PATCH. Poll the status-only endpoint until rendered_tree_revision equals tree_revision:

async function waitForRender(adId, { intervalMs = 2000, timeoutMs = 120000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const r = await fetch(`${API}/v1/image-ads/${adId}/render-status`, { headers });
    const { data } = await r.json();
    if (data.render_status === "failed") throw new Error(data.render_error?.message ?? "Render failed");
    if (data.rendered_tree_revision === data.tree_revision) return data.image_url;
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  throw new Error("Timed out waiting for the render");
}

const imageUrl = await waitForRender(adId);

GET /v1/image-ads/:id/render-status returns { id, tree_revision, rendered_tree_revision, render_status, render_error, image_url, thumbnail_url, updated_at }, without the full tree.

List an ad's edits

const r = await fetch(`${API}/v1/image-ads/${adId}/edits`, { headers });
const { data: edits } = await r.json(); // newest first

Errors

StatusCodeMeaning
400VALIDATION_ERRORSend exactly one of instruction or script
402INSUFFICIENT_BALANCEThe wallet can't cover an instruction edit
404IMAGE_AD_NOT_FOUNDUnknown ad id
409REVISION_CONFLICTbase_revision is not the current tree_revision. Fetch the outline again and retry.
422IMAGE_AD_NOT_COMPLETEDThe ad hasn't finished generating

An edit that runs but cannot be applied is not an HTTP error: it returns 200 with status: "failed" and a failure.

Prompt for your agent

Read https://www.staticadslab.com/docs/guides/design-edits.mdx and https://www.staticadslab.com/docs/resources/design-scripts.mdx. Write editAd(adId, change) that fetches GET /v1/image-ads/:id/outline, writes a design script for the change, sends it to POST /v1/image-ads/:id/edits as { script }, and if status is failed or warnings come back, fixes the script and tries again (at most 3 times). Then poll GET /v1/image-ads/:id/render-status every 2 seconds until rendered_tree_revision equals tree_revision and return image_url.