Streaming agent sessions
The automated editing page covers the simple fire-and-poll flow. This page covers the streaming alternative: instead of polling a job, you open an SSE (Server-Sent Events) connection and receive the agent’s responses in real time. This gives you token-by-token output and interactive control when the agent wants a decision.
Use the streaming API when you want to show live progress to your users, or when you need to approve or reject the agent’s plan before it runs.
For unattended batch processing where you don’t need real-time feedback, the simpler automated editing flow is usually the better choice.
Overview
The streaming flow has three concepts:
- Project. Holds the timeline and media. Created with
POST /v1/projects. - Thread. A conversation history tied to a project. Lets the agent keep context across multiple prompts.
- Session. A single agent run within a thread. You send a prompt, the agent streams back its response, and the project gets edited.
#Step 1: Create a project and thread
/v1/agent/threadsFirst, create a project (or use an existing one), then open a thread on it.
Save the returned thread_id. You can also fetch the most recent thread for a project with GET /v1/agent/threads/last?project_id=....
curl -X POST "https://api.rendley.com/v1/agent/threads" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "prj_123..."
}'const res = await fetch("https://api.rendley.com/v1/agent/threads", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
"project_id": "prj_123..."
}),
});
const { data } = await res.json();import requests
res = requests.post(
"https://api.rendley.com/v1/agent/threads",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"project_id": "prj_123..."
},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/agent/threads");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["project_id" => "prj_123..."]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
"bytes"
)
func main() {
body := bytes.NewBufferString(`{
"project_id": "prj_123..."
}`)
req, _ := http.NewRequest("POST", "https://api.rendley.com/v1/agent/threads", body)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/agent/threads")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
req["Content-Type"] = "application/json"
req.body = { project_id: "prj_123..." }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]The project to attach this thread to.
Response
{
"data": {
"id": "thr_xyz..."
}
}#Step 2: Start a session
/v1/agent/sessionsSend your prompt as message. The response is a text/event-stream, not JSON, so read it as a stream.
curl -X POST "https://api.rendley.com/v1/agent/sessions" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"thread_id": "thr_xyz...",
"project_id": "prj_123...",
"message": "Edit this interview into a 3-minute reel with captions and music."
}'const res = await fetch("https://api.rendley.com/v1/agent/sessions", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
"thread_id": "thr_xyz...",
"project_id": "prj_123...",
"message": "Edit this interview into a 3-minute reel with captions and music."
}),
});
const { data } = await res.json();import requests
res = requests.post(
"https://api.rendley.com/v1/agent/sessions",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"thread_id": "thr_xyz...",
"project_id": "prj_123...",
"message": "Edit this interview into a 3-minute reel with captions and music."
},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/agent/sessions");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["thread_id" => "thr_xyz...", "project_id" => "prj_123...", "message" => "Edit this interview into a 3-minute reel with captions and music."]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
"bytes"
)
func main() {
body := bytes.NewBufferString(`{
"thread_id": "thr_xyz...",
"project_id": "prj_123...",
"message": "Edit this interview into a 3-minute reel with captions and music."
}`)
req, _ := http.NewRequest("POST", "https://api.rendley.com/v1/agent/sessions", body)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/agent/sessions")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
req["Content-Type"] = "application/json"
req.body = { thread_id: "thr_xyz...", project_id: "prj_123...", message: "Edit this interview into a 3-minute reel with captions and music." }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]The thread from step 1.
The project the agent edits.
What you want, in plain language. Up to 500,000 characters.
Media to use, each { "media_id": "..." }. Up to 20.
#Step 3: Read the stream
/v1/agent/sessionsThe stream emits named events. Keep reading until you get completed (success) or error.
| Event | Meaning |
|---|---|
token | A chunk of the agent’s text reply. data is { "content": "..." }. |
token_done | The text reply is finished. |
interrupt | The agent is pausing for a decision (see step 4). |
completed | The agent finished. The project is now edited. |
error | The agent stopped. data carries the reason. |
const res = await fetch("https://api.rendley.com/v1/agent/sessions", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
thread_id: "THREAD_ID",
project_id: "PROJECT_ID",
message: "Edit this interview into a 3-minute reel with captions and music.",
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const chunks = buffer.split("\n\n");
buffer = chunks.pop();
for (const chunk of chunks) {
const event = /event: (.*)/.exec(chunk)?.[1];
const data = /data: (.*)/.exec(chunk)?.[1];
if (event === "token") process.stdout.write(JSON.parse(data).content);
if (event === "interrupt") { /* handle interrupt, see step 4 */ }
if (event === "completed") console.log("\nDone");
if (event === "error") throw new Error(data);
}
}import json, requests
with requests.post(
"https://api.rendley.com/v1/agent/sessions",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"thread_id": "THREAD_ID",
"project_id": "PROJECT_ID",
"message": "Edit this interview into a 3-minute reel with captions and music.",
},
stream=True,
) as res:
event = None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
data = line[6:]
if event == "token":
print(json.loads(data)["content"], end="")
elif event == "completed":
print("\nDone")
elif event == "error":
raise RuntimeError(data)#Step 4: Handle interrupts
/v1/agent/sessions/resumeThe agent may pause and emit an interrupt event when it wants your input. Common reasons:
- Plan approval. The agent shows what it intends to do and waits for your OK.
- Paid action approval. An action that costs credits needs confirmation.
- Clarification. The agent needs more information to continue.
Resume the agent by posting to the resume endpoint, then read the new stream the same way.
response can be "approve", "reject", or free text answering the agent’s question. For an unattended backend, auto-approve every interrupt to let the agent run to completion.
curl -X POST "https://api.rendley.com/v1/agent/sessions/resume" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"thread_id": "thr_xyz...",
"interrupt_id": "int_abc...",
"response": "approve"
}'const res = await fetch("https://api.rendley.com/v1/agent/sessions/resume", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
"thread_id": "thr_xyz...",
"interrupt_id": "int_abc...",
"response": "approve"
}),
});
const { data } = await res.json();import requests
res = requests.post(
"https://api.rendley.com/v1/agent/sessions/resume",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"thread_id": "thr_xyz...",
"interrupt_id": "int_abc...",
"response": "approve"
},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/agent/sessions/resume");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["thread_id" => "thr_xyz...", "interrupt_id" => "int_abc...", "response" => "approve"]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
"bytes"
)
func main() {
body := bytes.NewBufferString(`{
"thread_id": "thr_xyz...",
"interrupt_id": "int_abc...",
"response": "approve"
}`)
req, _ := http.NewRequest("POST", "https://api.rendley.com/v1/agent/sessions/resume", body)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/agent/sessions/resume")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
req["Content-Type"] = "application/json"
req.body = { thread_id: "thr_xyz...", interrupt_id: "int_abc...", response: "approve" }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]The thread of the interrupted session.
The ID from the interrupt event.
"approve", "reject", or free text answering the agent.
Step 5: Export
When the session completes, the project is edited but not yet rendered. Export it and poll the job to get a downloadable file.
Continuing a conversation
To send follow-up prompts, start another session on the same thread. The agent keeps the full context of previous edits:
curl -N -X POST https://api.rendley.com/v1/agent/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"thread_id": "THREAD_ID",
"project_id": "PROJECT_ID",
"message": "Make the intro shorter and add a call-to-action at the end."
}'