Async jobs (polling and SSE)
How to track design template and image ad generation — polling, batch polling, and Server-Sent Events.
POST /v1/image-ads and POST /v1/design-templates are asynchronous. They return 202 Accepted with the resource in processing and the pipeline runs in the background. This guide covers the three ways to know when it's done.
Status lifecycle
stateDiagram-v2
[*] --> processing: POST returns 202
processing --> completed: outputs ready
processing --> failed: error populated
completed --> [*]
failed --> [*]There are no intermediate states. The progress field updates during processing with pipeline-stage info; that's a UX hint, not a state machine.
You're billed only on completed. Failed jobs do not deduct from your wallet.
Polling
The default. Periodically GET the resource until status changes.
async function waitForCompletion(imageAdId, apiKey) {
while (true) {
const r = await fetch(
`https://api.staticadslab.com/v1/image-ads/${imageAdId}`,
{ headers: { "X-API-Key": apiKey } },
);
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, 4000));
}
}import time
import requests
def wait_for_completion(image_ad_id: str, api_key: str) -> dict:
while True:
r = requests.get(
f"https://api.staticadslab.com/v1/image-ads/{image_ad_id}",
headers={"X-API-Key": api_key},
)
data = r.json()["data"]
if data["status"] == "completed":
return data
if data["status"] == "failed":
raise Exception(data["error"]["message"])
time.sleep(4)while true; do
RESULT=$(curl -s https://api.staticadslab.com/v1/image-ads/ia_YOUR_ID \
-H "X-API-Key: YOUR_API_KEY")
STATUS=$(echo "$RESULT" | jq -r '.data.status')
echo "Status: $STATUS"
[ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break
sleep 4
done
echo "$RESULT" | jq '.data.image_url'Polling tips
- 4 seconds is a good interval for image ads (generation runs ~30–60 seconds).
- 2 seconds works for design templates (faster pipeline).
- Don't poll from the browser. Poll server-side and push updates to clients over your own channel.
- Always cap retries. Add a timeout (e.g. 5 minutes) and throw if exceeded — never poll forever.
Batch polling
When you have many ads in flight, ask for all their statuses in one request:
const ids = ["ia_a", "ia_b", "ia_c"].join(",");
const r = await fetch(`https://api.staticadslab.com/v1/image-ads?ids=${ids}`, {
headers: { "X-API-Key": process.env.SAL_API_KEY },
});
const { data } = await r.json();
const stillProcessing = data.filter((d) => d.status === "processing");This is the right pattern for batch generation. See Batch generation.
Server-Sent Events
For low-latency progress, subscribe to an SSE stream by adding Accept: text/event-stream to the GET request.
SSE is available for image ads:
GET /v1/image-ads/:id
Accept: text/event-streamEvent types
| Event | When it fires | Data |
|---|---|---|
started | Stream connected, job is queued | { "id": "..." } |
progress | Pipeline advances a step | { "step": "...", "message": "...", "percentage": 0.5 } |
complete | Job finished successfully | { "id": "..." } |
error | Job failed | { "code": "...", "message": "..." } |
heartbeat | Every 15 seconds to keep connection alive | Empty |
Subscribe to progress
const imageAdId = "ia_YOUR_IMAGE_AD_ID";
const response = await fetch(
`https://api.staticadslab.com/v1/image-ads/${imageAdId}`,
{
headers: {
"X-API-Key": process.env.SAL_API_KEY,
Accept: "text/event-stream",
},
},
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
readLoop: while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let sep;
while ((sep = buffer.indexOf("\n\n")) !== -1) {
const block = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2);
let eventName = "message";
let dataStr = "";
for (const line of block.split("\n")) {
if (line.startsWith("event:")) eventName = line.slice(6).trim();
else if (line.startsWith("data:")) dataStr += line.slice(5).trimStart();
}
if (eventName === "heartbeat") continue;
const data = dataStr ? JSON.parse(dataStr) : null;
console.log(eventName, data);
if (eventName === "complete" || eventName === "error") break readLoop;
}
}import json
import sseclient
import requests
response = requests.get(
f"https://api.staticadslab.com/v1/image-ads/{image_ad_id}",
headers={
"X-API-Key": api_key,
"Accept": "text/event-stream",
},
stream=True,
)
client = sseclient.SSEClient(response)
for event in client.events():
if event.event == "progress":
print(json.loads(event.data)["message"])
elif event.event == "complete":
break
elif event.event == "error":
raise RuntimeError(event.data)After complete (or once the connection drops), do one final GET to read the image_url.
Choosing a pattern
| Use this | When |
|---|---|
| Polling | One or a few ads, simple control flow, retries are fine |
| Batch polling | Tens or hundreds of ads in parallel |
| SSE | Customer-facing app where progress UI matters; one ad at a time |
Pitfalls
- Don't poll faster than every 2 seconds — you'll hit the rate limit.
- Reading
image_urlwhilestatus === "processing"returnsnull. Wait forcompleted. - SSE connections can drop on flaky networks. Always do a final
GETafter the stream closes to confirm state. - A
PATCHon an editable image ad sends the resource back toprocessing. Treat patches as new async jobs.