Quickstart
This guide takes about 15 minutes. You will send a first Turn to an AI employee, then try a workflow Run and a webhook once each.
What you need
- An UpServe account with organization OWNER permission. Only an OWNER can create Projects, issue keys, and register webhooks.
- A Project approved for the Private Beta. Until it is approved, every
/v1path returns 404. If you want to join, contact the UpServe team. - At least one Deployment to call. You create it by publishing one of your organization’s AI employees.
- An API key that allows that Deployment. The key is shown only once, when it is created.
You create Projects, publish Deployments, issue keys, and register webhooks in the web app under Settings > Developer (https://upserve.app/settings/developer). That screen is a management screen that works with the signed-in owner’s permission. An API key cannot perform management tasks.
Handle the key only through environment variables. Never leave it in code, logs, or repositories.
Step 1: Make your first Turn call
The call flow has four steps.
- Check which Deployments this key can call with
GET /v1/deployments. - Create a Session and its first Turn with
POST /v1/deployments/{deployment_id}/sessions. The server stores the acceptance and then answers202. - Send the same request again with the same
Idempotency-Key. It does not run again and returns the same acceptance. - Poll
GET /v1/sessions/{session_id}/turns/{turn_id}until the Turn reaches a terminal status.
curl
export UPSERVE_API_KEY="up_..."
export BASE="https://api.upserve.app"
# 1. List the Deployments this key may call
curl -s "$BASE/v1/deployments" \
-H "Authorization: Bearer $UPSERVE_API_KEY"
# 2. Create a Session and its first Turn (202 Accepted)
curl -s -X POST "$BASE/v1/deployments/$DEPLOYMENT_ID/sessions" \
-H "Authorization: Bearer $UPSERVE_API_KEY" \
-H "Idempotency-Key: demo-0001" \
-H "Content-Type: application/json" \
-d '{"input":[{"type":"TEXT","text":"Summarize this month sales."}]}'
# 3. Send the exact same request again: the same receipt comes back
# 4. Poll the Turn until it reaches a terminal status
curl -s "$BASE/v1/sessions/$SESSION_ID/turns/$TURN_ID" \
-H "Authorization: Bearer $UPSERVE_API_KEY"The 202 response (receipt) looks like this.
{
"session_id": "6f1c0a52-0000-4000-8000-000000000001",
"turn_id": "6f1c0a52-0000-4000-8000-000000000002",
"status": "QUEUED",
"idempotency_expires_at": "2026-10-06T09:00:00+00:00",
"status_url": "/v1/sessions/6f1c0a52-0000-4000-8000-000000000001/turns/6f1c0a52-0000-4000-8000-000000000002",
"events_url": "/v1/sessions/6f1c0a52-0000-4000-8000-000000000001/events"
}Idempotency-Key is required for create, input, cancel, and upload requests. If a network failure kept you from reading the response, send the same body with the same key again. If the request was already accepted, the same receipt comes back and the work does not run twice. Sending a different body with the same key returns 409 IDEMPOTENCY_CONFLICT. To ask for completed work again, use a new key.
Python
import os
import time
import uuid
import httpx
BASE = "https://api.upserve.app"
AUTH = {"Authorization": "Bearer " + os.environ["UPSERVE_API_KEY"]}
DONE = {"SUCCEEDED", "FAILED", "CANCELED", "EXPIRED", "REQUIRES_ACTION"}
def create_session(deployment_id: str, text: str, key: str) -> dict:
response = httpx.post(
f"{BASE}/v1/deployments/{deployment_id}/sessions",
headers={**AUTH, "Idempotency-Key": key},
json={"input": [{"type": "TEXT", "text": text}]},
timeout=30,
)
response.raise_for_status()
return response.json()
def wait_turn(session_id: str, turn_id: str) -> dict:
while True:
response = httpx.get(f"{BASE}/v1/sessions/{session_id}/turns/{turn_id}", headers=AUTH, timeout=30)
response.raise_for_status()
turn = response.json()
if turn["status"] in DONE:
return turn
time.sleep(2)
# Reuse the same key when you resend after a network failure
key = str(uuid.uuid4())
receipt = create_session(os.environ["DEPLOYMENT_ID"], "Summarize this month sales.", key)
turn = wait_turn(receipt["session_id"], receipt["turn_id"])
print(turn["status"], turn["usage"])
for output in turn["outputs"]:
if output["type"] == "TEXT":
print(output["text"])TypeScript
This uses the built-in fetch of Node 22 or later.
const BASE = 'https://api.upserve.app';
const AUTH = { Authorization: `Bearer ${process.env.UPSERVE_API_KEY}` };
const DONE = new Set(['SUCCEEDED', 'FAILED', 'CANCELED', 'EXPIRED', 'REQUIRES_ACTION']);
async function createSession(deploymentId: string, text: string, key: string) {
const response = await fetch(`${BASE}/v1/deployments/${deploymentId}/sessions`, {
method: 'POST',
headers: { ...AUTH, 'Idempotency-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({ input: [{ type: 'TEXT', text }] }),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
return response.json();
}
async function waitTurn(sessionId: string, turnId: string) {
for (;;) {
const response = await fetch(`${BASE}/v1/sessions/${sessionId}/turns/${turnId}`, { headers: AUTH });
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
const turn = await response.json();
if (DONE.has(turn.status)) return turn;
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
// Reuse the same key when you resend after a network failure
const receipt = await createSession(process.env.DEPLOYMENT_ID!, 'Summarize this month sales.', crypto.randomUUID());
const turn = await waitTurn(receipt.session_id, receipt.turn_id);
console.log(turn.status, turn.usage);A successful Turn carries its results in outputs, as below. If usage.status is PENDING, the cost is not final yet.
{
"id": "6f1c0a52-0000-4000-8000-000000000002",
"session_id": "6f1c0a52-0000-4000-8000-000000000001",
"status": "SUCCEEDED",
"usage": {"status": "FINAL", "total_su": 1.25},
"error": null,
"created_at": "2026-10-05T09:00:00+00:00",
"started_at": "2026-10-05T09:00:02+00:00",
"completed_at": "2026-10-05T09:00:20+00:00",
"outputs": [
{"id": "o1", "key": "answer", "type": "TEXT", "visibility": "FINAL",
"availability": "READY", "item_id": "i1", "text": "This month sales were up 8%."}
],
"next_cursor": null
}Complete examples with retries, backoff, and SSE reconnection are available as runtime_client.py, runtime_client.ts, and runtime_quickstart.sh. Webhook receiver examples are runtime_webhook_receiver.py and runtime_webhook_receiver.ts.
Step 2: Run a workflow
You call a published workflow with a Run, not a Session. The flow is as follows.
- (Optional) Upload an input file with
POST /v1/filesand get afile_id. - Send the published version and the input to
POST /v1/workflows/{workflow_id}/runs. You get202and arun_id. - Poll
GET /v1/runs/{run_id}for status and progress, or receive it over SSE. There is no final result before the Run ends. - When the Run ends, read the JSON, file, and URL results from
GET /v1/runs/{run_id}/outputs. - For a file result, issue a 60 second URL with
POST /v1/artifacts/{artifact_id}/downloadand download right away.
# 1. (Optional) Upload an input file and keep the returned file_id
curl -s -X POST "$BASE/v1/files" \
-H "Authorization: Bearer $UPSERVE_API_KEY" \
-H "Idempotency-Key: file-0001" \
-F "file=@report.csv;type=text/csv"
# 2. Start a Run on a published version (202 Accepted)
curl -s -X POST "$BASE/v1/workflows/$WORKFLOW_ID/runs" \
-H "Authorization: Bearer $UPSERVE_API_KEY" \
-H "Idempotency-Key: run-0001" \
-H "Content-Type: application/json" \
-d '{"published_version":3,"input":{"count":3},"file_ids":["'"$FILE_ID"'"]}'
# 3. Poll the Run status and progress
curl -s "$BASE/v1/runs/$RUN_ID" \
-H "Authorization: Bearer $UPSERVE_API_KEY"
# 4. Read the outputs (JSON, FILE and URL items), following next_cursor if present
curl -s "$BASE/v1/runs/$RUN_ID/outputs?limit=50" \
-H "Authorization: Bearer $UPSERVE_API_KEY"
# 5. Issue a short-lived download URL for a FILE output, then fetch it right away
curl -s -X POST "$BASE/v1/artifacts/$ARTIFACT_ID/download" \
-H "Authorization: Bearer $UPSERVE_API_KEY"
curl -s -o report.pdf "$BASE$DOWNLOAD_URL"The detailed rules for each step are in Workflows and Runs.
Step 3: Get completion notifications
Use a webhook to be told when work finishes instead of polling. You register the webhook as an HTTPS address in the developer screen, and you keep the secret, which is shown once at registration, on your receiving server. The receiver verifies the signature, drops duplicates by event_id, and then re-reads the current state with an authenticated request. See webhooks for the verification code.
Next
- Key permissions and rotation: authentication
- Error handling and retry principles: errors
- Limits and retention: limits and data