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 reference ad, 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
The platform may set batch_id on image ad rows for certain workflows. You can filter the list endpoint when present:
GET /v1/image-ads?batch_id=batch_2026_04_30_run_42This is independent of ids= and is useful for retroactive batch reads.
Note: Client-supplied
batch_idonPOST /v1/image-adsis on the roadmap. For now, 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.