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:
- Rules of the road — what every integration must obey.
- Common pitfalls — specific mistakes agents tend to make, with corrected code.
- Prompt library — copy-paste prompts for common tasks.
- 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.comThere is no /v2 and no per-region endpoint. Always use this base URL.
Authentication
X-API-Key: sal_live_<key>- Header is
X-API-Key. NotAuthorization: Bearer. - Keys start with
sal_live_. - Server-side only. Never put a key in browser code.
- Create keys at https://www.staticadslab.com/settings?tab=api-keys.
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_idwhen 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:
| Prefix | Resource |
|---|---|
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.
- POST returns
202 Acceptedwith the resource instatus: "processing". - The pipeline runs in the background.
- Resource transitions to
status: "completed"(with output) orstatus: "failed"(witherror: { code, message }). - There are no intermediate states. Just
processing → completedorprocessing → failed.
Track progress two ways:
- Polling —
GET /v1/<resource>/:idevery 4 seconds (image ads) or 2 seconds (design templates). - SSE —
GET /v1/image-ads/:idwithAccept: text/event-stream.
See Async jobs for code.
Image ad SKUs
| SKU | Cost | Output |
|---|---|---|
flat_image_ad | $1.00 | A single 4K Meta-ready PNG. current_json_tree is null. |
editable_image_ad | $4.00 | PNG 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". Otherwise422 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_BALANCEwith a top-up URL.
Errors
| Status | Code | When |
|---|---|---|
400 | VALIDATION_ERROR | Invalid request body or query |
401 | UNAUTHORIZED | Missing or invalid API key |
402 | INSUFFICIENT_BALANCE | Wallet balance too low |
404 | NOT_FOUND | Resource doesn't exist or isn't in this org |
413 | PAYLOAD_TOO_LARGE | Body > 15 MB |
422 | Various | Entity relationship issues (product not in brand, template not completed, etc.) |
429 | RATE_LIMIT_EXCEEDED | Too many requests; check Retry-After header |
500 | INTERNAL_ERROR | Server-side failure |
See Errors for the full table and remediation tips.
Image uploads
- Images are uploaded via
POST /v1/imagesasmultipart/form-data. Field name:file. - Never embed base64 image data in JSON request bodies. The API rejects it.
- For
imageFillUrlin JSON trees andnode_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_cThis 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
processingwithout 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/:idon a flat ad — only editable ads have acurrent_json_tree. - Don't pass camelCase field names. Snake_case only.
- Don't skip checking
data.status— every async response can beprocessingand 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 returneddata.srcURL, 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
| Artifact | When to use |
|---|---|
https://www.staticadslab.com/llms.txt | Index of every doc page with curated rules. Always start here. |
https://www.staticadslab.com/llms-full.txt | Entire docs concatenated. Use when the agent has a large context window and you want one-shot grounding. |
https://www.staticadslab.com/docs/<slug>.mdx | A 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:
| Page | Markdown URL |
|---|---|
| Quickstart | https://www.staticadslab.com/docs/quickstart.mdx |
| Image ads | https://www.staticadslab.com/docs/resources/image-ads.mdx |
| Editable image ads | https://www.staticadslab.com/docs/resources/editable-image-ads.mdx |
| Async jobs | https://www.staticadslab.com/docs/guides/async-jobs.mdx |
| This page | https://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.