Static Ads Lab

Using LLMs

A single page for AI coding agents. The rules of the road, common pitfalls, copy-paste prompts, and tips for Claude Code, Cursor, and Codex.

Static Ads Lab is designed to be driven by AI coding agents. This page is the single artifact to hand to Claude Code, Cursor, or Codex before they write integration code.

This page is intentionally dense. It rolls up four things into one:

  1. Rules of the road — what every integration must obey.
  2. Common pitfalls — specific mistakes agents tend to make, with corrected code.
  3. Prompt library — copy-paste prompts for common tasks.
  4. Tips for AI coding assistants — how to drive Claude Code, Cursor, or Codex effectively.

If you only have time for one section, read Rules of the road.


Rules of the road

Identity

Static Ads Lab is an API platform for generating Meta image ads programmatically. It is not:

  • A creative SaaS app (we sell the API, not a UI).
  • A general image generator (it's tuned for Meta ad formats).
  • A wrapper around an LLM (it's a multi-step AI pipeline with layout detection, copy generation, and visual polish).

Base URL

https://api.staticadslab.com

There is no /v2 and no per-region endpoint. Always use this base URL.

Authentication

X-API-Key: sal_live_<key>

Response envelope

Every response is wrapped:

{
  "data": ...,
  "meta": { "request_id": "req_...", "timestamp": "..." }
}

Errors:

{
  "error": { "code": "...", "message": "..." },
  "meta": { "request_id": "req_...", "timestamp": "..." }
}
  • All field names are snake_case. Not camelCase.
  • Always include meta.request_id when filing support tickets.
  • List responses also include has_more: boolean.

ID prefixes

Every resource has a typed ID prefix. Always pass the prefixed string, never raw UUIDs:

PrefixResource
brand_Brand
prod_Product
pv_Product variant
aud_Audience
img_Image
dt_Design template
ia_Image ad
iav_Image ad version
ied_Image ad design edit
sal_live_API key

Mixing prefixes (e.g., passing a prod_ where a brand_ is expected) returns 422.

Async pattern

POST /v1/image-ads and POST /v1/design-templates are asynchronous.

  1. POST returns 202 Accepted with the resource in status: "processing".
  2. The pipeline runs in the background.
  3. Resource transitions to status: "completed" (with output) or status: "failed" (with error: { code, message }).
  4. There are no intermediate states. Just processing → completed or processing → failed.

Track progress with polling:

  • Polling — GET /v1/<resource>/:id every 4 seconds (image ads) or 2 seconds (design templates).

See Async jobs for code.

Every image ad is editable

Every ad comes back as a Meta-ready PNG plus a layered JSON tree (current_json_tree) you can edit and re-render. There is no flat mode and no editable request field (sending it returns 400 VALIDATION_ERROR).

WhatCost
Image ad (SKU editable_image_ad), including 1 generated image asset$2.00
Each additional generated image asset$0.50
Design edit with an instructionModel cost + 30%, rounded up to the cent
Design edit with your own scriptFree

Only successful generations count; retries and failures are free. The ad is billed once, when it completes. Ads take up to about 20 minutes. The ad's aspect ratio follows the reference ad (1:1, 4:5 or 9:16).

Editing an ad

  • Design edits: POST /v1/image-ads/:id/edits with { "instruction": "..." } or { "script": "..." }. Synchronous, atomic, a few seconds. Read GET /v1/image-ads/:id/outline first to target layers. See Edit ads with instructions or scripts.
  • Whole-tree saves: PATCH /v1/image-ads/:id with the full current_json_tree. See Edit and re-render.
  • Either way the image re-renders in the background. Poll GET /v1/image-ads/:id/render-status until rendered_tree_revision equals tree_revision.

Required inputs to generate an ad

POST /v1/image-ads
{
  "reference_ad_url": "https://cdn.example.com/reference-ad.png",
  "brand_id": "brand_...",
  "product_id": "prod_...",
  "audience_id": "aud_..."
}
  • The product must belong to the brand. Otherwise 422 PRODUCT_BRAND_MISMATCH.
  • reference_ad_url must be a publicly accessible image URL. Otherwise 422 REFERENCE_AD_URL_INVALID.

Optional: product_variant_id, prompt.

Wallet billing

  • Each successful generation deducts from the org's wallet.
  • Failed jobs do not deduct.
  • If wallet is low, POST returns 402 INSUFFICIENT_BALANCE with a top-up URL.

Errors

StatusCodeWhen
400VALIDATION_ERRORInvalid request body or query
401UNAUTHORIZEDMissing or invalid API key
402INSUFFICIENT_BALANCEWallet balance too low
404NOT_FOUNDResource doesn't exist or isn't in this org
413PAYLOAD_TOO_LARGEBody > 15 MB
422VariousEntity relationship issues (product not in brand, invalid reference ad URL, etc.)
429RATE_LIMIT_EXCEEDEDToo many requests; check Retry-After header
500INTERNAL_ERRORServer-side failure

See Errors for the full table and remediation tips.

Image uploads

  • Images are uploaded via POST /v1/images as multipart/form-data. Field name: file.
  • Never embed base64 image data in JSON request bodies. The API rejects it.
  • For reference_ad_url and image paints in JSON trees (sal.url on an IMAGE fill), always use a publicly accessible URL.

Batch polling

If generating many ads, don't poll each ID separately. Poll all of them in one call:

GET /v1/image-ads?ids=ia_a,ia_b,ia_c

This returns the full list with statuses in a single request.

Pagination

Cursor-based via starting_after=<last_item_id>. There is no cursor parameter — use starting_after. Default limit is 20; max is 100. Trust has_more, not data.length.

Idempotency

Send Idempotency-Key: <unique-string> on POST and PATCH to make retries safe. Cached for 24 hours per API key. Replays return Idempotent-Replayed: true. Concurrent in-flight requests with the same key return 409 IDEMPOTENCY_CONFLICT.

Things you should never do

  • Don't poll faster than every 2 seconds.
  • Don't loop on processing without a backoff or max-attempts cap.
  • Don't call the API from a browser.
  • Don't embed base64 image data in JSON.
  • Don't assume IDs are stable across orgs (they're scoped per org).
  • Don't send editable when creating an ad. Every ad is editable, and the field is rejected.
  • Don't download image_url right after an edit or PATCH. Wait until rendered_tree_revision equals tree_revision.
  • Don't invent JSON tree fields. The tree is a strict subset of Figma's Plugin API node shape (schema v2) and unknown keys are rejected. See JSON trees.
  • Don't pass camelCase field names. Snake_case only.
  • Don't skip checking data.status — every async response can be processing and not yet have outputs.

Common pitfalls

This list comes from real mistakes we've seen AI coding agents make.

camelCase vs snake_case

Wrong:

body: JSON.stringify({
  referenceAdUrl: "https://cdn.example.com/reference-ad.png",
  brandId: "brand_...",
})

Right:

body: JSON.stringify({
  reference_ad_url: "https://cdn.example.com/reference-ad.png",
  brand_id: "brand_...",
})

The API rejects camelCase. Every public field is snake_case.

Authorization: Bearer instead of X-API-Key

Wrong:

headers: { Authorization: `Bearer ${key}` }

Right:

headers: { "X-API-Key": key }

There is no Bearer flow. The header is X-API-Key and the value is the raw sal_live_… key.

Polling without a backoff or cap

Wrong:

while (data.status === "processing") {
  data = await getAd(id);
}

That's a tight loop. It will hammer the API.

Right:

async function waitForCompletion(id, { intervalMs = 4000, timeoutMs = 25 * 60_000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const r = await fetch(`https://api.staticadslab.com/v1/image-ads/${id}`, {
      headers: { "X-API-Key": process.env.SAL_API_KEY },
    });
    const { data } = await r.json();
    if (data.status === "completed") return data;
    if (data.status === "failed") throw new Error(data.error?.message ?? "failed");
    await new Promise((res) => setTimeout(res, intervalMs));
  }
  throw new Error("Timed out waiting for completion");
}

Polling each ad individually instead of batching

Wrong:

await Promise.all(adIds.map((id) => fetch(`/v1/image-ads/${id}`)));

Right:

const ids = adIds.join(",");
const r = await fetch(`https://api.staticadslab.com/v1/image-ads?ids=${ids}`, { headers });

One request, all statuses. Much cheaper.

Sending editable: true

Older examples set "editable": true on POST /v1/image-ads. The field no longer exists: every ad is editable, and sending it returns 400 VALIDATION_ERROR. Remove it.

Using the removed AI edit endpoint

POST /v1/image-ads/:id/edit and version restore are gone. To change an ad, send { "instruction": "..." } or { "script": "..." } to POST /v1/image-ads/:id/edits (note the plural). It is synchronous: no version to poll, just wait for the background render.

Writing a JSON tree in the retired shape

Editable ads use JSON tree schema v2: { schemaVersion: 2, colorProfile: "SRGB", root }, in Figma's Plugin API node shape. Agents trained on older examples often write v1 fields.

Wrong:

headline.fontFamily = "Inter";
headline.fontWeight = 700;
headline.x = 72;
photo.fills = [{ type: "IMAGE", imageFillUrl: "https://cdn.example.com/photo.jpg" }];

Right:

headline.styledTextSegments = headline.styledTextSegments.map((segment) => ({
  ...segment,
  fontName: { family: "Inter", style: "Bold" },
}));
headline.relativeTransform[0][2] = 72; // x = m02
photo.fills = [{ type: "IMAGE", scaleMode: "FILL", sal: { url: "https://cdn.example.com/photo.jpg" } }];

Unknown keys return 400 VALIDATION_ERROR with the exact path. Use a family and style from Supported fonts; any other font renders in Inter at the closest style. Text segments must cover every character. Trees from before schema v2 (no schemaVersion) are returned as stored but cannot be patched; regenerate the ad.

Reading image_url while the ad is still processing

Right after POST /v1/image-ads, the ad is status: "processing" and image_url: null. If your code logs the URL immediately, it's null. Wait for status: "completed" first.

PATCH /v1/image-ads/:id and design edits are similar: the tree is saved right away, but image_url stays the previous render until the background render finishes. Poll GET /v1/image-ads/:id/render-status until rendered_tree_revision equals tree_revision before reading image_url.

Invalid reference_ad_url

reference_ad_url must be a publicly accessible image URL. The API re-hosts the image before queueing generation. Private URLs, HTML pages, or unreachable hosts return 422 REFERENCE_AD_URL_INVALID.

Upload product or brand images via POST /v1/images first, then use the returned src URL — or host the reference image on your own CDN.

Mixing IDs across organizations

API keys are scoped to one organization. If you copy an ID from one workspace into a key from another, you get 404 NOT_FOUND.

Embedding base64 images in JSON

The API rejects base64 image data in JSON request bodies. If you need to provide an image:

  • Upload it via POST /v1/images (multipart) and use the returned data.src URL, or
  • Provide your own publicly-accessible URL.

This applies to reference_ad_url, JSON tree image paints (sal.url), brand logos, and product variant images alike.

Calling the API from a browser

Don't. API keys carry full org permissions. Move the call to your backend and proxy from there.

Using a wallet that's empty

POST /v1/image-ads returns 402 INSUFFICIENT_BALANCE if the wallet can't cover the request. The error message includes the top-up URL — surface it to your end user (or top up programmatically out-of-band before retrying).

Forgetting Content-Type: application/json on POST/PATCH

The body parser requires it for JSON requests. Multipart uploads (/v1/images) don't need it; the browser/client sets the right boundary header.

Not surfacing the request_id to your logs

Every response includes meta.request_id. Logging it makes debugging massively easier when you contact support. Add it to your structured logs.

log.error("Static Ads Lab request failed", {
  endpoint,
  request_id: body?.meta?.request_id,
  code: body?.error?.code,
  message: body?.error?.message,
});

Prompt library

Drop these into your AI coding assistant. Every prompt assumes the agent can fetch URLs.

Learn the API

Read https://www.staticadslab.com/llms.txt and tell me what Static Ads Lab is. Then read https://www.staticadslab.com/docs/agents.mdx and acknowledge the rules. Use these as your reference for any code you write.

Generate your first ad

Read https://www.staticadslab.com/docs/quickstart.mdx and walk me through generating my first Meta image ad. I have an API key. Use JavaScript with fetch and the X-API-Key header. Don't forget to poll until the ad is completed.

Build a batch generator

Read https://www.staticadslab.com/docs/agents.mdx and https://www.staticadslab.com/docs/guides/batch-generation.mdx. I want a script that takes a list of audience IDs and generates one image ad per audience for a given product. Use parallel POSTs and batch polling via GET /v1/image-ads?ids=... Server-side only — read the API key from env.

Adapt a cookbook recipe

Read this cookbook recipe: https://github.com/staticadslab/cookbook/tree/main/recipes/reviews-to-persona-image-ads. I want to adapt it for my use case. Instead of customer reviews, I have a CSV of product descriptions and I want to generate one image ad per product. Walk me through what to change.

Edit an ad with a script

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 reads GET /v1/image-ads/:id/outline, writes a design script for the change, sends it to POST /v1/image-ads/:id/edits as { script }, retries with a fixed script if status is failed (at most 3 times), then polls GET /v1/image-ads/:id/render-status until rendered_tree_revision equals tree_revision and returns image_url.

Edit and re-render

Read https://www.staticadslab.com/docs/resources/editable-image-ads.mdx and https://www.staticadslab.com/docs/resources/json-trees.mdx. Write a function patchHeadline(adId, newHeadline) that updates the headline text node in an image ad (rebuilding styledTextSegments so they cover the new text) saves it with PATCH (sending base_revision), then polls GET every 2 seconds with a timeout until rendered_tree_revision equals tree_revision and returns the new image_url. Find the headline node by its current characters value.

Upload a brand from scratch via API

Read https://www.staticadslab.com/docs/resources/brands.mdx and https://www.staticadslab.com/docs/resources/products.mdx. I have a JSON file with my brand identity (name, description, hex colors, font name, logo PNG path) and a list of products. Build a script that creates the brand, uploads the logo via POST /v1/images, attaches it, then creates each product. Use snake_case field names.

Convert a reference ad screenshot into a design template

Read https://www.staticadslab.com/docs/resources/design-templates.mdx. I have a screenshot URL of a Meta ad I like. Write a function that creates a design template from this URL and polls every 2 seconds until status: completed. Throw on failed.

Sync to and from Figma

Read https://www.staticadslab.com/docs/figma-plugin.mdx and https://www.staticadslab.com/docs/guides/figma-sync.mdx. Walk me through using the Static Ads Lab Figma plugin to edit an existing editable image ad in Figma and sync the result back to the API.

Handle errors well

Read https://www.staticadslab.com/docs/reference/errors.mdx. Wrap my Static Ads Lab fetch calls in a helper that throws structured errors: a ValidationError for 400, AuthError for 401, BillingError for 402, RateLimitError for 429 (with retry support using the Retry-After header), and ServiceError for 5xx. Always include the request_id from response.meta in the thrown error.

Migrate from camelCase to snake_case

Read https://www.staticadslab.com/docs/agents.mdx. My existing code uses camelCase field names (referenceAdUrl, brandId, etc.) but the Static Ads Lab API uses snake_case. Find every Static Ads Lab fetch call in my project and convert the request bodies to snake_case.

Tips for AI coding assistants

Three artifacts to feed agents

ArtifactWhen to use
https://www.staticadslab.com/llms.txtIndex of every doc page with curated rules. Always start here.
https://www.staticadslab.com/llms-full.txtEntire docs concatenated. Use when the agent has a large context window and you want one-shot grounding.
https://www.staticadslab.com/docs/<slug>.mdxA single page as raw markdown. Use to focus the agent on one topic.

Page-as-markdown URLs

Every doc page is available at <page>.mdx for direct fetch. Useful for "read just this" prompts:

PageMarkdown URL
Quickstarthttps://www.staticadslab.com/docs/quickstart.mdx
Image adshttps://www.staticadslab.com/docs/resources/image-ads.mdx
Editable image adshttps://www.staticadslab.com/docs/resources/editable-image-ads.mdx
Design editshttps://www.staticadslab.com/docs/guides/design-edits.mdx
Design scriptshttps://www.staticadslab.com/docs/resources/design-scripts.mdx
Async jobshttps://www.staticadslab.com/docs/guides/async-jobs.mdx
This pagehttps://www.staticadslab.com/docs/agents.mdx

Best practices

  • Include your real IDs. When prompting, paste your actual brand ID, product ID, audience ID, and reference ad URL. The agent can write working code on the first try.
  • Ask the agent to read before writing. A prompt like "read the docs first, then build" produces better results than "build me an integration".
  • Point to specific pages for specific tasks. Editing ads? Tell the agent to read the design edits and design scripts pages rather than the entire docs.
  • Always require server-side code. Open prompts with "Server-side only — read the API key from env."
  • Have it acknowledge the rules. Ask the agent to summarize the rules before writing code. If it gets one wrong (e.g. confuses scopes for product slugs), correct it before letting it generate.

Cookbook for AI agents

The Static Ads Lab cookbook on GitHub is a collection of full-stack sample apps. Each recipe includes a prompt.md that you can hand directly to your assistant — it contains the project's intent, architecture, edit points, and suggested next tasks.