Jobs & streaming
A website, an app or a video shot takes real time to make. Every call that builds something lets you choose how to wait — and turns are durable, so a dropped connection never cancels the work.
#Three ways to wait
| Send | You get | Use it when |
|---|---|---|
| nothing (default) | The finished result, in one response. | Scripts and servers that can hold a request open. |
stream: true | Server-sent events as the work happens, then result. | You want to show progress to a person. |
background: true | 202 at once, with an id to check on. | Agents, batches, queues, serverless functions with short limits. |
A waiting call stays open as long as the work does — the server allows up to 800 seconds. Give your HTTP client a timeout of at least 15 minutes, or use stream or background.
#Streaming
With stream: true the response is text/event-stream. Each event is event: <name> and one data: <json> line. A Studio turn sends turn, plan and status as the director works; the last event is always result — the same body a waiting call returns — or error (message and request_id). Then the stream closes.
event: turn
data: {"id": "1e0f…", "status": "building", "studio": "image", …}
event: plan
data: {…}
event: status
data: {…}
event: result
data: {"id": "1e0f…", "object": "studio.turn", "status": "completed", …}curl -N -X POST "https://app.attria.io/api/v1/studio/image/turns" \
-H "Authorization: Bearer $ATTRIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"message": "Two scroll-stopping images for a summer iced-latte promo, bright and minimal", "auto": true, "stream": true}'// Server-sent events: blocks split by a blank line, each with "event:" and "data:".
async function streamTurn(message: string) {
const res = await fetch("https://app.attria.io/api/v1/studio/image/turns", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATTRIA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ message, auto: true, stream: true }),
});
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) throw new Error("stream closed before a result");
buffer += value;
let end;
while ((end = buffer.indexOf("\n\n")) >= 0) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const event = /^event: (.*)$/m.exec(block)?.[1];
const data = JSON.parse(/^data: (.*)$/m.exec(block)?.[1] ?? "null");
if (event === "result") return data; // the same body a waiting call returns
if (event === "error") throw new Error(`${data.message} (${data.request_id})`);
console.log(event, data); // turn, plan, status — progress to show a person
}
}
}import json, os, uuid, requests
def stream_turn(message):
res = requests.post(
"https://app.attria.io/api/v1/studio/image/turns",
headers={
"Authorization": f"Bearer {os.environ['ATTRIA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"message": message, "auto": True, "stream": True},
stream=True,
timeout=900,
)
event = None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
data = json.loads(line[6:])
if event == "result":
return data # the same body a waiting call returns
if event == "error":
raise RuntimeError(f"{data['message']} ({data['request_id']})")
print(event, data) # turn, plan, status — progress to show a person
raise RuntimeError("stream closed before a result")Show or log the events before result as progress, but don't build logic on their contents — only result and error are the contract.
#Background and polling
With background: true a Studio turn answers 202 Accepted as soon as it's claimed, with its id. Poll GET /v1/turns/{id} every 5–10 seconds until status is no longer building.
curl -X POST "https://app.attria.io/api/v1/studio/image/turns" \
-H "Authorization: Bearer $ATTRIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"message": "Two scroll-stopping images for a summer iced-latte promo, bright and minimal",
"auto": true,
"background": true
}'const res = await fetch("https://app.attria.io/api/v1/studio/image/turns", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATTRIA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
message: "Two scroll-stopping images for a summer iced-latte promo, bright and minimal",
auto: true,
background: true,
}),
});
const turn = await res.json();import os, uuid, requests
res = requests.post(
"https://app.attria.io/api/v1/studio/image/turns",
headers={
"Authorization": f"Bearer {os.environ['ATTRIA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"message": "Two scroll-stopping images for a summer iced-latte promo, bright and minimal",
"auto": True,
"background": True,
},
timeout=30,
)
turn = res.json()// Poll a background turn every 5–10 s until it stops building.
async function waitForTurn(id: string) {
for (let delay = 5000; ; delay = 10000) {
const res = await fetch(`https://app.attria.io/api/v1/turns/${id}`, {
headers: { Authorization: `Bearer ${process.env.ATTRIA_API_KEY}` },
});
const turn = await res.json();
if (turn.status !== "building") return turn;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}import os, time, requests
def wait_for_turn(turn_id):
"""Poll a background turn every 5-10 s until it stops building."""
delay = 5
while True:
turn = requests.get(
f"https://app.attria.io/api/v1/turns/{turn_id}",
headers={"Authorization": f"Bearer {os.environ['ATTRIA_API_KEY']}"},
timeout=30,
).json()
if turn["status"] != "building":
return turn
time.sleep(delay)
delay = 10Never start a second copy of a turn because the first seems slow — poll it. The work continues whether or not you're polling, and it's in the Studio the whole time.
#How each kind of work is checked on
| Work | Check on it with |
|---|---|
| Studio turns | GET /v1/turns/{id} |
| A new website's build | GET /v1/websites/{id} — build.status and build.stage |
| Agent runs (background) | The same request again, with the same Idempotency-Key — 202 while it runs. Or GET /v1/runs/{id}. |
| Video content apps | The same request again, with the same Idempotency-Key — it reads the job and moves it forward. |
Repeating a request with its key is safe by design: it replays or reads, never runs twice. See Idempotency.
#Slow endpoints
These can take minutes, and are marked slow in the reference:
- POST
/v1/studio/website/turnsTalk to the Website Director - POST
/v1/studio/document/turnsTalk to the Documents director - POST
/v1/studio/app/turnsTalk to the App Builder - POST
/v1/studio/image/turnsTalk to the Images director - POST
/v1/studio/video/turnsTalk to the Video director - POST
/v1/studio/{studio}/apps/{app}Run a Studio Content App - POST
/v1/agentsCreate an Agent from a description - PATCH
/v1/agents/{id}Change an Agent - POST
/v1/agents/{id}/activateTurn an Agent on - POST
/v1/agents/{id}/runsTalk to an Agent (run it) - POST
/v1/approvals/{id}Approve or reject an Agent action