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.

Bearer authStreaming chatOne turn at a time

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.com

Authentication

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_busy

Posting 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.

PlanActive Conversations
Free1
Pro10
Business50

Streaming protocol

The POST .../chat response body IS the stream (Vercel AI SDK line protocol).

Line PrefixMeaning
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

POST/v1/agent/cloud/conversations

Create a conversation. JSON body: agent (general|us-finance|zh-legal), optional model, optional title.

GET/v1/agent/cloud/conversations

List your conversations, newest first. Query: limit, offset.

GET/v1/agent/cloud/conversations/{id}

Get a conversation: summary + ordered messages + files.

PATCH/v1/agent/cloud/conversations/{id}

Rename a conversation. JSON body: title.

DELETE/v1/agent/cloud/conversations/{id}

Archive a conversation.

POST/v1/agent/cloud/conversations/{id}/chat

Send 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.

GET/v1/agent/cloud/conversations/{id}/status

Poll the live turn: monotonic answer + append-only tool_calls + status. The drop-resilient fallback to streaming.

POST/v1/agent/cloud/conversations/{id}/cancel

Cancel the active turn.

GET/v1/agent/cloud/conversations/{id}/workspace/files

List the conversation workspace tree (uploads/ + output/).

POST/v1/agent/cloud/conversations/{id}/workspace/files

Upload files to the workspace. multipart/form-data: path, files[].

GET/v1/agent/cloud/conversations/{id}/workspace/files/{path}

Download a workspace file.

DELETE/v1/agent/cloud/conversations/{id}/workspace/files/{path}

Delete a workspace file or directory.

POST/v1/agent/cloud/conversations/{id}/workspace/mkdir

Create a workspace directory. JSON body: path.

GET/v1/agent/cloud/conversations/{id}/artifacts/{name}

Download a durable artifact (survives archive).

Agents

GET/v1/agent/cloud/agents

List available agents and your subscription status.

Commands

GET/v1/agent/cloud/commands

Slash-command palette for an agent. Query: agent.

Other

GET/v1/agent/cloud/spec

The model catalog (models + reasoning efforts) for the composer.

GET/v1/agent/cloud/api-spec

This 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.

NameTypeRequiredDescription
messagestring (form field)yesThe user message.
modelstring (form field)noModel override for this turn. See GET .../spec for available models.
reasoning_effortstring (form field)noReasoning effort level, when the model supports it (low, medium, high).
filesfile (form field)noFile 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.

StatusTerminalMeaning
queuednoWaiting for an available execution slot.
runningnoAgent is executing. answer may contain partial text.
doneyesTurn completed. answer has the full response.
erroryesTurn failed. error field has the message.
timeoutyesServer-side execution timed out.
cancelledyesCancelled 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.

StatusMeaningAction
401Invalid or expired tokenCheck your API key or re-login.
402Insufficient creditsAdd credits in your dashboard.
403Subscription required (vertical agent)Contact us for access.
404Conversation not foundCheck the conversation id.
409Conversation busy (turn running)Wait for the current turn or POST .../cancel.
429Rate limitedBack 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 answer

2. 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 answer

3. 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())