Batch generation
Generate many image ads in parallel and batch-poll their status efficiently.
You'll often want to generate many ads at once — one per audience, one per variant, one per design template, etc. This guide shows the right way to fire them in parallel and check their status without hammering the API.
Pattern
flowchart LR
Inputs[List of input combos] --> POSTs[POST /v1/image-ads in parallel]
POSTs --> Wait[Wait, then batch-poll]
Wait --> Group[GET /v1/image-ads?ids=...]
Group --> Done{All done?}
Done -->|No| Wait
Done -->|Yes| Out[Return completed ads]Sketch
const SAL_BASE = "https://api.staticadslab.com";
const headers = {
"X-API-Key": process.env.SAL_API_KEY,
"Content-Type": "application/json",
};
async function generateBatch(inputs) {
const submissions = await Promise.all(
inputs.map((input) =>
fetch(`${SAL_BASE}/v1/image-ads`, {
method: "POST",
headers,
body: JSON.stringify(input),
}).then((r) => r.json()),
),
);
const adIds = submissions.map((s) => s.data.id);
return waitForBatch(adIds);
}
async function waitForBatch(adIds, { intervalMs = 4000, timeoutMs = 600_000 } = {}) {
const deadline = Date.now() + timeoutMs;
const completed = new Map();
while (completed.size < adIds.length && Date.now() < deadline) {
const pending = adIds.filter((id) => !completed.has(id));
const url = `${SAL_BASE}/v1/image-ads?ids=${pending.join(",")}`;
const r = await fetch(url, { headers });
const { data } = await r.json();
for (const ad of data) {
if (ad.status === "completed" || ad.status === "failed") {
completed.set(ad.id, ad);
}
}
if (completed.size < adIds.length) {
await new Promise((res) => setTimeout(res, intervalMs));
}
}
if (completed.size < adIds.length) {
throw new Error(`Timed out — ${adIds.length - completed.size} still processing`);
}
return adIds.map((id) => completed.get(id));
}Throttling
Don't fire 1,000 POSTs at once. The API will rate-limit you. Throttle to a sensible parallelism (e.g., 8 concurrent submissions):
import pLimit from "p-limit";
const limit = pLimit(8);
const submissions = await Promise.all(
inputs.map((input) =>
limit(() =>
fetch(`${SAL_BASE}/v1/image-ads`, {
method: "POST",
headers,
body: JSON.stringify(input),
}).then((r) => r.json()),
),
),
);Idempotency
If your batch may be retried (cron, webhook handler, etc.), use stable idempotency keys per input:
const idempotencyKey = `batch:${batchId}:${inputIndex}`;Same key + same body → cached response on retry. No duplicate wallet charges.
Track via batch_id
When you set the same batch_id on multiple POST /v1/image-ads calls (server-side), you can later filter the list endpoint by it:
GET /v1/image-ads?batch_id=batch_2026_04_30_run_42This is independent of ids= and is useful for retroactive batch reads.
Note:
batch_idis currently set by the platform on certain workflows; client-sidebatch_idprovisioning is on the roadmap. For now, the safe pattern is to track ad IDs in your own queue/DB and pass them to?ids=.
Pitfalls
- Don't poll each ad individually — batch poll. Polling each costs N requests instead of 1.
- Don't fire all submissions in parallel without throttling — you'll get
429. - Hard-cap your wait loop with a timeout — never wait forever.
- Wallet may run out mid-batch. Catch
402 INSUFFICIENT_BALANCEand surface it cleanly.