Cloud Agent API
UcoWorker Cloud Agent API
One agent, two I/O ports. The web chat and this API drive the SAME conversations: a conversation opened in either appears in the web sidebar, and both share your per-plan caps and credit balance.
Web and API are the same conversation
One agent, two I/O ports. A conversation opened from the browser or from this API is the same conversation: it shows up in your web sidebar, shares the same per-plan active-conversation caps, and draws from the same credit pool. Continue any chat from either channel with the same uc_sk_ key or session.
https://api.ucoworker.comAuthentication
Send a Bearer token in the Authorization header. A dedicated uc_sk_ API key is recommended for scripts and CI; a session JWT also works.
Authorization header
# Recommended: dashboard API key (shown once at creation) Authorization: Bearer uc_sk_... # Or: session JWT from login Authorization: Bearer <your_access_token>
- uc_sk_ API key (recommended)
- session JWT
Quickstart
Create a conversation, then post a turn. The chat response streams the agent reply line by line (see Streaming below). Add -F "model=..." or -F "files=@path" to pick a model or attach files.
Create
# 1. Start a conversation (returns its id)
curl -X POST "https://api.ucoworker.com/v1/agent/cloud/conversations" \
-H "Authorization: Bearer $UCOWORKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent": "general"}'Chat
# 2. Send a turn -- the response body IS the stream
curl -N -X POST \
"https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/chat" \
-H "Authorization: Bearer $UCOWORKER_API_KEY" \
-F "message=Research recent open-source LLM releases and summarize the top 3, with links"Concurrency
A conversation runs ONE turn at a time, enforced across every channel (web + API).
409conversation_busyPosting a turn while a conversation is already running returns 409 conversation_busy. Wait for the in-flight turn to finish (poll .../status for a terminal state) before sending the next one.
Limits
Active conversations are capped per plan and shared across the web chat and the API. Each turn holds 50 credits, reconciled to actual usage when the turn finishes.
| Plan | Active Conversations |
|---|---|
| Free | 1 |
| Pro | 10 |
| Business | 50 |
Streaming protocol
The POST .../chat response body IS the stream (Vercel AI SDK line protocol).
| Line Prefix | Meaning |
|---|---|
| 0:"text" | answer text delta |
| 3:"msg" | error |
| a:{...} | tool completed |
| b:{...} | tool started |
| d:{...} | turn finished |
Resilience
If the long-lived stream drops mid-turn, reconnect by polling GET .../status (monotonic answer + append-only tool_calls) until a terminal status (done|error|timeout|cancelled).
Endpoints
Conversations & Chat
/v1/agent/cloud/conversationsCreate a conversation. JSON body: agent (general|us-finance|zh-legal), optional model, optional title.
/v1/agent/cloud/conversationsList your conversations, newest first. Query: limit, offset.
/v1/agent/cloud/conversations/{id}Get a conversation: summary + ordered messages + files.
/v1/agent/cloud/conversations/{id}Rename a conversation. JSON body: title.
/v1/agent/cloud/conversations/{id}Archive a conversation.
/v1/agent/cloud/conversations/{id}/chatSend a turn. multipart/form-data: message, optional model, optional reasoning_effort, optional files[]. The response body IS the stream (Vercel AI line protocol). Returns 409 conversation_busy if a turn is already running.
/v1/agent/cloud/conversations/{id}/statusPoll the live turn: monotonic answer + append-only tool_calls + status. The drop-resilient fallback to streaming.
/v1/agent/cloud/conversations/{id}/cancelCancel the active turn.
/v1/agent/cloud/conversations/{id}/workspace/filesList the conversation workspace tree (uploads/ + output/).
/v1/agent/cloud/conversations/{id}/workspace/filesUpload files to the workspace. multipart/form-data: path, files[].
/v1/agent/cloud/conversations/{id}/workspace/files/{path}Download a workspace file.
/v1/agent/cloud/conversations/{id}/workspace/files/{path}Delete a workspace file or directory.
/v1/agent/cloud/conversations/{id}/workspace/mkdirCreate a workspace directory. JSON body: path.
/v1/agent/cloud/conversations/{id}/artifacts/{name}Download a durable artifact (survives archive).
Agents
/v1/agent/cloud/agentsList available agents and your subscription status.
Commands
/v1/agent/cloud/commandsSlash-command palette for an agent. Query: agent.
Other
/v1/agent/cloud/specThe model catalog (models + reasoning efforts) for the composer.
/v1/agent/cloud/api-specThis document: the cloud-agent API surface, auth, limits, and the streaming protocol.
Chat request details
The chat endpoint uses multipart/form-data (not JSON) so you can attach files in the same request. The response body is the stream -- the connection stays open until the turn finishes.
| Name | Type | Required | Description |
|---|---|---|---|
| message | string (form field) | yes | The user message. |
| model | string (form field) | no | Model override for this turn. See GET .../spec for available models. |
| reasoning_effort | string (form field) | no | Reasoning effort level, when the model supports it (low, medium, high). |
| files | file (form field) | no | File attachments. Repeat the field for multiple files. |
Examples
# Simple message
curl -N -X POST "https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/chat" \
-H "Authorization: Bearer $UCOWORKER_API_KEY" \
-F "message=Summarize the uploaded file"
# With file attachment
curl -N -X POST "https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/chat" \
-H "Authorization: Bearer $UCOWORKER_API_KEY" \
-F "message=Analyze this spreadsheet" \
-F "files=@report.xlsx"
# With model override
curl -N -X POST "https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/chat" \
-H "Authorization: Bearer $UCOWORKER_API_KEY" \
-F "message=Deep analysis" \
-F "model=claude-sonnet-4-20250514"Use -N (--no-buffer) with curl to see streamed tokens in real time.
Only one turn runs per conversation at a time. A second /chat while a turn is running returns 409 conversation_busy.
Status values
Poll GET .../conversations/{id}/status for the authoritative turn state. The answer field grows monotonically and tool_calls is append-only, so printing the delta since your last poll is safe.
| Status | Terminal | Meaning |
|---|---|---|
| queued | no | Waiting for an available execution slot. |
| running | no | Agent is executing. answer may contain partial text. |
| done | yes | Turn completed. answer has the full response. |
| error | yes | Turn failed. error field has the message. |
| timeout | yes | Server-side execution timed out. |
| cancelled | yes | Cancelled by POST .../cancel. |
Response shape
{
"status": "done",
"answer": "The full agent response text...",
"tool_calls": [
{"id": "tc_1", "name": "web_search", "status": "done", "detail": {...}, "result": {...}}
],
"updated_at": "2026-06-06T10:01:30Z",
"queue_position": null,
"error": null
}Workspace files
Each conversation has an isolated workspace. The agent writes output files here, and you can upload files for the agent to read. Two root directories: uploads/ (your files) and output/ (agent-generated).
List files
curl "https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/workspace/files" \
-H "Authorization: Bearer $UCOWORKER_API_KEY"
# Response
# {
# "root": "/workspace",
# "entries": [
# {"path": "output/report.json", "type": "file", "size": 1234, "mtime": "..."},
# {"path": "uploads/data.csv", "type": "file", "size": 5678, "mtime": "..."}
# ]
# }Download a file
JSON files are returned parsed. Binary files return raw bytes. URL-encode each path segment but keep / separators literal.
# JSON file
curl "https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/workspace/files/output/report.json" \
-H "Authorization: Bearer $UCOWORKER_API_KEY"
# Binary file
curl -o report.xlsx \
"https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/workspace/files/output/report.xlsx" \
-H "Authorization: Bearer $UCOWORKER_API_KEY"Upload files
Upload into uploads/ or output/. Max 50 MB per file. For large folders, batch ~10 files per request.
curl -X POST "https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/workspace/files" \
-H "Authorization: Bearer $UCOWORKER_API_KEY" \
-F "path=uploads" \
-F "files=@data.csv" \
-F "files=@config.json"
# Response
# {"saved": [{"path": "uploads/data.csv", "size": 5678}, ...]}Download an artifact
Named artifacts generated during a turn (documents, images) are available at a separate path.
curl -o summary.pdf \
"https://api.ucoworker.com/v1/agent/cloud/conversations/{id}/artifacts/summary.pdf" \
-H "Authorization: Bearer $UCOWORKER_API_KEY"Conversation lifecycle
A conversation is a persistent thread. Create it once, send as many turns as you need over hours or days, then archive when done.
create ──> chat ──> [running] ──> done
│ │
│ (stream drops?) │
│ ↓ │
│ poll /status ────────│
│ │
└── send another /chat ─┘ (resume)
delete ───────────> (archive)Stream + poll (recommended pattern)
Stream the response for real-time tokens. If the connection drops (network timeout, proxy reset), the turn keeps running server-side. Switch to polling /status every 1--2 seconds until it reaches a terminal state. The answer field grows monotonically, so printing the delta since your last read is safe.
Persistent conversations
Store the conv_id locally and reuse it. The agent retains the full history. Resume a conversation hours or days later with another /chat call -- the agent picks up where it left off.
Web and API share state
The same conversation is accessible from both the web chat and the API. Start in the browser, continue from a script, or vice versa.
Workspace files persist
Files in the workspace persist for the lifetime of the conversation. Upload files before a turn and download agent-generated output after.
Error codes
Error bodies are JSON with a detail field. The 402 quota body also includes used, limit, and credits fields.
| Status | Meaning | Action |
|---|---|---|
| 401 | Invalid or expired token | Check your API key or re-login. |
| 402 | Insufficient credits | Add credits in your dashboard. |
| 403 | Subscription required (vertical agent) | Contact us for access. |
| 404 | Conversation not found | Check the conversation id. |
| 409 | Conversation busy (turn running) | Wait for the current turn or POST .../cancel. |
| 429 | Rate limited | Back off and retry with exponential delay. |
Python examples
Production patterns using httpx (pip install httpx). These examples work with any agent type.
1. Basic: create, chat, and get the answer
import httpx, json, os
BASE = "https://api.ucoworker.com/v1/agent/cloud"
AUTH = {"Authorization": f"Bearer {os.environ['UCOWORKER_API_KEY']}"}
# Create a conversation
conv = httpx.post(f"{BASE}/conversations", headers=AUTH,
json={"agent": "general"}).json()
conv_id = conv["id"]
# Stream the response
answer = ""
with httpx.stream("POST", f"{BASE}/conversations/{conv_id}/chat",
headers=AUTH,
files=[("message", (None, "What is 2+2?"))],
timeout=httpx.Timeout(None, connect=15)) as resp:
for line in resp.iter_lines():
if not line.strip():
continue
kind, _, payload = line.partition(":")
if kind == "0":
chunk = json.loads(payload)
answer += chunk
print(chunk, end="", flush=True)
elif kind in ("d", "3"):
break
# Confirm via status
status = httpx.get(f"{BASE}/conversations/{conv_id}/status",
headers=AUTH).json()
final = status.get("answer") or answer2. Robust: stream with poll fallback
Long-running turns may outlast the HTTP connection. This pattern streams when possible and falls back to polling when the stream drops.
import httpx, json, time, os
BASE = "https://api.ucoworker.com/v1/agent/cloud"
AUTH = {"Authorization": f"Bearer {os.environ['UCOWORKER_API_KEY']}"}
TERMINAL = {"done", "error", "timeout", "cancelled"}
def chat_and_wait(conv_id: str, message: str, model: str | None = None) -> str:
"""Send a message, stream the response, poll if the stream drops."""
parts = [("message", (None, message))]
if model:
parts.append(("model", (None, model)))
answer = ""
finished = False
try:
with httpx.stream("POST", f"{BASE}/conversations/{conv_id}/chat",
headers=AUTH, files=parts,
timeout=httpx.Timeout(None, connect=15)) as resp:
resp.raise_for_status()
for line in resp.iter_lines():
if not line.strip():
continue
kind, _, payload = line.partition(":")
if kind == "0":
answer += json.loads(payload)
elif kind in ("d", "3"):
finished = True
break
except (httpx.HTTPError, httpx.StreamError):
pass # stream dropped -- fall through to poll
if not finished:
for _ in range(400):
status = httpx.get(f"{BASE}/conversations/{conv_id}/status",
headers=AUTH).json()
if status.get("status") in TERMINAL:
return status.get("answer") or answer
time.sleep(1.5)
return answer3. Persistent: resume a conversation across sessions
Store the conversation id locally and reuse it. The agent remembers the full history.
import json, os, httpx
STATE_FILE = ".agent_state.json"
def get_or_create_conv(label: str, agent: str = "general") -> str:
"""Reuse an existing conversation or create a new one."""
state = {}
if os.path.exists(STATE_FILE):
state = json.load(open(STATE_FILE))
conv_id = state.get(label)
if conv_id:
return conv_id
resp = httpx.post(f"{BASE}/conversations", headers=AUTH,
json={"agent": agent, "title": label})
resp.raise_for_status()
conv_id = resp.json()["id"]
state[label] = conv_id
json.dump(state, open(STATE_FILE, "w"), indent=2)
return conv_id
# First run: creates a new conversation
conv_id = get_or_create_conv("daily-research")
answer = chat_and_wait(conv_id, "What happened in tech today?")
# Next run (hours/days later): resumes the same conversation
conv_id = get_or_create_conv("daily-research")
answer = chat_and_wait(conv_id, "Any updates since last time?")4. Structured output: agent writes files, you download them
Ask the agent to write output to a specific path, then download the file. List the workspace as a fallback if the agent uses a different path.
# Tell the agent to write output
chat_and_wait(conv_id, 'Write a JSON summary to output/summary.json')
import time; time.sleep(2) # brief wait for file persistence
# Try the expected path
resp = httpx.get(
f"{BASE}/conversations/{conv_id}/workspace/files/output/summary.json",
headers=AUTH
)
if resp.status_code == 404:
# Fallback: list workspace and find it
tree = httpx.get(
f"{BASE}/conversations/{conv_id}/workspace/files",
headers=AUTH
).json()
for entry in tree.get("entries", []):
if entry["path"].endswith("summary.json"):
resp = httpx.get(
f"{BASE}/conversations/{conv_id}/workspace/files/{entry['path']}",
headers=AUTH
)
break
if resp.status_code == 200:
data = resp.json()
print(json.dumps(data, indent=2))5. Concurrent: run multiple conversations in parallel
Each conversation is independent. Run them concurrently with asyncio, respecting your plan's concurrency limit.
import asyncio, httpx, json, os
BASE = "https://api.ucoworker.com/v1/agent/cloud"
MAX_CONCURRENT = 5
async def run_task(client, sem, name, prompt):
async with sem:
resp = await client.post(f"{BASE}/conversations",
json={"agent": "general", "title": name})
conv_id = resp.json()["id"]
answer = ""
async with client.stream(
"POST", f"{BASE}/conversations/{conv_id}/chat",
files=[("message", (None, prompt))],
timeout=httpx.Timeout(None, connect=15)
) as r:
async for line in r.aiter_lines():
kind, _, payload = line.partition(":")
if kind == "0":
answer += json.loads(payload)
elif kind in ("d", "3"):
break
return {"task": name, "answer": answer}
async def main():
auth = {"Authorization": f"Bearer {os.environ['UCOWORKER_API_KEY']}"}
sem = asyncio.Semaphore(MAX_CONCURRENT)
tasks = [
("Task A", "Summarize recent AI news"),
("Task B", "Top Python libraries for data science?"),
("Task C", "Explain quantum computing simply"),
]
async with httpx.AsyncClient(headers=auth) as client:
results = await asyncio.gather(
*[run_task(client, sem, n, p) for n, p in tasks]
)
for r in results:
print(f"--- {r['task']} ---")
print(r["answer"][:200])
asyncio.run(main())