Cumulus TalosDocs
Open Talos
On this page

Using the API

Everything in Talos is an HTTPS operation on one host. This page takes you from an API key to a published agent, a cloud run you follow live and a phone call, with each step in curl, TypeScript and Python.

Quickstart

1. Get an API key

In Talos → Settings → API keys, name the key after the system that will use it, pick its role and create it. The secret (cmls_…) is shown once: store it in your secret manager. Only organization admins manage keys; an admin can also create them with POST /api/v1/keys.

Key roleCanUse it for
memberRead everything; create and edit agents; publish; start, update and cancel runs.Starting runs and calls (the default role)
adminEverything a member can, plus API keys, secrets, SIP trunks, phone numbers and the budget.Setup and automation that manages the organization
viewerRead only.Dashboards and exports

Runs need a member or admin key

A viewer key gets 403 forbidden on every command. Each operation's required role is in the tables on these pages and in the API reference.

2. Call the API

The base URL is https://api.cumuluslabs.io for every operation. Send the key as a bearer token in the Authorization header. Request and response bodies are JSON.

export CUMULUS_API_KEY=cmls_...   # a member or admin key
curl -sS "https://api.cumuluslabs.io/api/v1/agents?limit=20" \
  -H "Authorization: Bearer $CUMULUS_API_KEY"

A 401 unauthenticated means the key is missing, revoked or expired. The SDKs read CUMULUS_API_KEY and default to this base URL; CUMULUS_BASE_URL overrides it.

Idempotency-Key

Commands that create something take an Idempotency-Key header, so a retry never creates a second run, agent or key. Send a new random value (a UUID) for each logical request and reuse it for every retry of that request.

  • Same key, same body: you get the original resource back, with 200 instead of 201. Nothing is created twice.
  • Same key, different body: 409 idempotency_conflict.
  • No key: 400 invalid_request. Keys are 1 to 256 characters; agents.create and agents.clone need 8 to 255 visible ASCII characters. A UUID satisfies both.
  • API keys are the exception: a replayed keys.create or keys.rotate answers 409 conflict (details.reason: "secret_returned_once"), because a key's secret is only ever returned once.

The SDKs generate a key for you when you pass none, and report it on every error. These operations take one:

POST``agents.create``POST``agents.clone``POST``agents.import``POST``runs.create``PATCH``runs.update``POST``runs.send_message``POST``test_sessions.create``POST``trunks.create``POST``numbers.import``PUT``secrets.put``POST``test_suites.create``POST``test_runs.create``POST``keys.create``POST``keys.rotate

The other commands are idempotent by what they name: publishing names the version, draft edits name the revision they expect, and deleting twice deletes once.

Errors and retries

Every failure has the same body. code is stable; branch on it, not on the message.

Error body

Example
{
  "error": {
    "code": "rate_limited",
    "message": "Tenant concurrent call limit reached",
    "retryable": true,
    "outcome": "none",
    "details": { "reason": "capacity_exhausted" }
  }
}
  • retryable: the same request can succeed later (back off first).
  • outcome: "none" means nothing happened. "unknown" means the command may have taken effect: retry it with the same Idempotency-Key, never a new one.
  • details.reason says why, for example capacity_exhausted, spend_cap_exhausted, trunk_not_authorized or missing_dynamic_variables. Validation errors list details.issues; a draft that does not compile lists details.diagnostics.
codeHTTPretryableoutcome
unauthenticated401nonone
forbidden403nonone
not_found404nonone
conflict409nonone
invalid_request400nonone
idempotency_conflict409nonone
policy_denied403nonone
capability_unavailable409nonone
provider_unavailable503yesnone
knowledge_unavailable409yesnone
publication_invalid422nonone
run_not_active409nonone
rate_limited429yesnone
outcome_unknown502yesunknown
internal500yesnone
# Admits a run, retrying transient failures with the SAME key. Prints the run, or the error and returns 1.
create_run() {  # $1: request body, $2: Idempotency-Key (keep it until you know the outcome)
  local code attempt
  for attempt in 1 2 3; do
    code=$(curl -sS -o run.json -w '%{http_code}' -X POST https://api.cumuluslabs.io/api/v1/runs \
      -H "Authorization: Bearer $CUMULUS_API_KEY" -H "Content-Type: application/json" \
      -H "Idempotency-Key: $2" -d "$1") || code=000
    case $code in
      200|201) cat run.json; return 0 ;;             # 201 created, 200 replayed
      000|429|5??) sleep $((attempt * 2)) ;;         # no answer, or retryable: try again with the same key
      *) jq . run.json >&2; return 1 ;;              # not retryable: read error.code
    esac
  done
  echo "No run confirmed after 3 attempts (HTTP $code); retry later with Idempotency-Key $2" >&2
  return 1
}
if run=$(create_run "$(jq -n --arg agent "$AGENT_ID" '{channel: "cloud", agent_id: $agent, mode: "task", input: {}}')" "$(uuidgen)"); then
  echo "$run" | jq '{run_id, state}'
fi

Pagination

List operations take limit (1 to 200, default 50) and cursor. A page carries next_cursor; pass it back as cursor for the next page, until it is null. Run events page by sequence instead (after_sequence and next_after_sequence).

args=(--data-urlencode "limit=200")
while :; do
  page=$(curl -sS --fail-with-body -G "https://api.cumuluslabs.io/api/v1/runs" \
    -H "Authorization: Bearer $CUMULUS_API_KEY" "${args[@]}") || { echo "request failed: $page" >&2; break; }
  echo "$page" | jq -r '.items[] | "\(.run_id) \(.state)"'
  cursor=$(echo "$page" | jq -r '.next_cursor // empty')
  [ -n "$cursor" ] || break
  args=(--data-urlencode "limit=200" --data-urlencode "cursor=$cursor")
done

Create and publish an agent

An agent starts as a draft. Edit it, then publish it as version 0; later publishes take the next version. The cloud_task template takes a JSON input, works through it and ends. See Agents for every template, edit and the flow graph.

# 1. Create the agent from a template. It exists as its first draft.
created=$(curl -sS -X POST https://api.cumuluslabs.io/api/v1/agents \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name": "Ticket triage", "template": "cloud_task", "channels": ["cloud"], "languages": ["en"]}')
AGENT_ID=$(echo "$created" | jq -r .ref.id)

# 2. Edit the draft: set the agent's prompt. expected_revision guards against concurrent edits.
edited=$(curl -sS -X PATCH "https://api.cumuluslabs.io/api/v1/agents/$AGENT_ID/draft" \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(echo "$created" | jq '{draft_id: .draft.draft_id, expected_revision: .draft.revision,
        edits: [{type: "flow.prompt", text: "Classify the support ticket in the input as billing, technical or other."}]}')")

# 3. Publish the draft as version 0. It becomes the head: runs that name no version use it.
curl -sS -X POST "https://api.cumuluslabs.io/api/v1/agents/$AGENT_ID/publications" \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(echo "$edited" | jq '{draft_id, expected_revision: .revision, version: 0}')" | jq '.error // {ref, is_head}'

Start a cloud run

POST /api/v1/runs admits a run of the agent's head (or of version, when you name one) and answers at once with the run in state admitted. mode is task or chat; input must match the publication's input schema. Add "test": true for a test run.

run=$(curl -sS -X POST https://api.cumuluslabs.io/api/v1/runs \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "$(jq -n --arg agent "$AGENT_ID" \
        '{channel: "cloud", agent_id: $agent, mode: "task", input: {ticket: "I was charged twice this month."}}')")
RUN_ID=$(echo "$run" | jq -r .run_id)
echo "$run" | jq '.error // {run_id, state, version}'

Follow a run live

GET /api/v1/runs/{run_id}/events with Accept: text/event-stream streams the run's semantic events as server-sent events, from the first. Each frame's id is the event's sequence (1, 2, 3, … per run), event is its type and data is the event as JSON. A comment line keeps the connection open every 25 seconds. The stream ends once the run has ended and its effects and webhooks have settled.

text/event-stream

Example
id: 1
event: run.admitted
data: {"event_id":"…","type":"run.admitted","sequence":1,"run":{"run_id":"…","publication":{…}},…}

id: 2
event: run.state_changed
data: {"event_id":"…","type":"run.state_changed","sequence":2,…}

: keepalive

To resume after a disconnect, send Last-Event-ID with the last sequence you processed (or the query parameter after_sequence). Without the SSE Accept header the same operation returns a JSON page:{ items, next_after_sequence }, up to limit events (1 to 500, default 100).

# Server-sent events. The stream ends after the run has ended and its effects have settled.
curl -sS -N "https://api.cumuluslabs.io/api/v1/runs/$RUN_ID/events" \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Accept: text/event-stream"

# Reconnect where you left off: send the id of the last event you processed.
curl -sS -N "https://api.cumuluslabs.io/api/v1/runs/$RUN_ID/events" \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: 3"

A run's events have these types:

run.admitted``run.state_changed``run.attempt_started``run.attempt_ended``run.node_entered``run.message``transcript.updated``run.ended``run.degraded``run.turn_events``phone.sip_received``phone.amd_verdict``phone.voicemail``phone.transfer``action.requested``action.decided``effect.sent``effect.settled``knowledge.retrieved``recording.updated``analysis.updated

Place a phone call

A phone run dials to from from, both in E.164 form. from is a number you imported on one of your SIP trunks; leave it out to use the caller ID the agent was published with. dynamic_variables fill the agent's {{variables}} for this call. Numbers and trunks are set up once, by an admin: see Phone.

curl -sS -X POST https://api.cumuluslabs.io/api/v1/runs \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "$(jq -n --arg agent "$PHONE_AGENT_ID" '{
        channel: "phone", agent_id: $agent,
        to: "+14155550123", from: "+14155550100",
        dynamic_variables: {customer_name: "Ada", balance: "120.00"},
        metadata: {crm_id: "A-1001"}
      }')" | jq '.error // {run_id, state, phone_state, to, from}'

phone_state follows the call (dialing, ringing, active, ended, …) while the run state stays channel-neutral. PATCH /api/v1/runs/{run_id} updates a live call's dynamic variables.

Send every variable the agent uses

If the agent's flow uses a {{variable}} you did not send, the call is refused before it dials: 400 invalid_request, details.reason: "missing_dynamic_variables", and the message names each missing one. Dynamic variables shows how to list what an agent needs.

Read the transcript, recording and analysis

Once a run has ended, read what it produced. Each read has a state, so you can tell "not yet" (pending, processing) from "never" (none, not_available, not_requested) and from "removed by retention" (expired).

# What a cloud task produced (validated against the publication's output schema)
curl -sS "https://api.cumuluslabs.io/api/v1/runs/$RUN_ID/result" -H "Authorization: Bearer $CUMULUS_API_KEY" | jq '.error // {state, result}'
# The conversation, turn by turn
curl -sS "https://api.cumuluslabs.io/api/v1/runs/$RUN_ID/transcript" -H "Authorization: Bearer $CUMULUS_API_KEY" \
  | jq -r '.error // (.utterances[] | "\(.role): \(.text)")'
# Post-run analysis: disposition, success and the fields the agent extracts
curl -sS "https://api.cumuluslabs.io/api/v1/runs/$RUN_ID/analysis" -H "Authorization: Bearer $CUMULUS_API_KEY" \
  | jq '.error // {state, disposition, successful, fields}'
# Phone and browser runs only: a recording URL that expires after five minutes
curl -sS "https://api.cumuluslabs.io/api/v1/runs/$RUN_ID/recording" -H "Authorization: Bearer $CUMULUS_API_KEY" \
  | jq '.error // {state, url, url_expires_at}'

Limits

  • Concurrent runs per channel are capped for your organization. A run beyond the cap is refused with 429 rate_limited (details.reason: "capacity_exhausted") and nothing is created. GET /api/v1/runs/capacity shows each channel's limit and active runs.
  • Budget: once metered spend reaches the organization's cap, new runs get 429 with spend_cap_exhausted.
  • Request rate: API key and secret operations are rate limited per organization; beyond the limit they answer 429 with a Retry-After header.
Content version 753c978aMarkdown source
Using the API · Cumulus Talos docs