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
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 two ways:

  • PollingGET /v1/<resource>/:id every 4 seconds (image ads) or 2 seconds (design templates).
  • SSEGET /v1/image-ads/:id with Accept: text/event-stream.

See Async jobs for code.

Image ad SKUs

SKUCostOutput
flat_image_ad$1.00A single 4K Meta-ready PNG. current_json_tree is null.
editable_image_ad$4.00PNG plus a JSON tree you can PATCH and re-render. Versioned.

Set "editable": true in the request body to get an editable ad. Default is flat.

Required inputs to generate an ad

POST /v1/image-ads
{
  "design_template_id": "dt_...",
  "brand_id": "brand_...",
  "product_id": "prod_...",
  "audience_id": "aud_..."
}
  • The product must belong to the brand. Otherwise 422 PRODUCT_BRAND_MISMATCH.
  • The design template must be in status: "completed". Otherwise 422 DESIGN_TEMPLATE_NOT_READY.

Optional: product_variant_id, prompt, editable, options.generate_ai_images, options.generate_copy, node_overrides.

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, template not completed, 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 imageFillUrl in JSON trees and node_overrides, 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 try PATCH /v1/image-ads/:id on a flat ad — only editable ads have a current_json_tree.
  • 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({
  designTemplateId: "dt_...",
  brandId: "brand_...",
})

Right:

body: JSON.stringify({
  design_template_id: "dt_...",
  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 = 180000 } = {}) {
  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.

Trying to PATCH a flat ad

Flat ads (editable: false, the default) have current_json_tree: null. There is no JSON tree to patch.

If you need to modify generated ads, request "editable": true at creation time. There is no path to convert a flat ad into an editable one — you'd have to regenerate.

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.

This also applies after PATCH /v1/image-ads/:id on an editable ad: the resource returns to processing while it re-renders.

Passing a processing design template

A design template must be in status: "completed" before it can be used to generate image ads. If you just created it via POST /v1/design-templates, poll it first:

async function waitForTemplate(id) {
  while (true) {
    const r = await fetch(`https://api.staticadslab.com/v1/design-templates/${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("Template failed");
    await new Promise((res) => setTimeout(res, 2000));
  }
}

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 node_overrides (imageFillUrl), 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 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 editable image ad and waits for the re-render to complete. 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 (designTemplateId, 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
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 design template ID. 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. Editable ads? Tell the agent to read the editable image ads page 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.