Developers · API
v1Running many agents
With dozens or hundreds of agents, the whole job is keeping your own records and the agents from drifting apart. There is one tool for it — externalId.
Tie your own record to the agent
When you create an agent, put your own identifier in externalId. It comes back in every list response, so you do not need a separate mapping table.
| Your system | The agent |
|---|---|
Your record dental_00123 | externalId: "dental_00123" |
The same value cannot be used twice. If it already exists you get 409 EXTERNAL_ID_EXISTS, so "try to create, and on 409 find it in the list" is the safe default flow.
Onboard in one go
The minimum sequence when a new business signs up. Two rules:
- Use a fresh idempotency key for every creation. Reusing a key replays the first response stored under it for as long as the key exists (at least 24 hours) — re-onboarding a business whose agent was terminated would give you that terminated agent's response. Put a value that is unique to one sign-up, such as your sign-up record's identifier, in the key, and reuse a key only when retrying that same request.
- Check the response and branch. Each
409means something different, and saving settings can fail too.
const BASE_URL = process.env.LS_BASE_URL;
const API_KEY = process.env.LS_API_KEY;
// Thrown when the same signupId should be tried again shortly. The caller catches it and retries.
class RetryLater extends Error {}
// signupId = your sign-up record's identifier. Reuse it on retries; a new sign-up gets a new value.
async function onboard(business, signupId) {
// 1) Create the agent
const created = await fetch(`${BASE_URL}/v1/agents`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `create-${business.id}-${signupId}`,
},
body: JSON.stringify({
name: business.name,
region: 'kr',
externalId: business.id,
}),
});
let agentId;
if (created.status === 201) {
agentId = (await created.json()).agentId;
} else if (created.status === 409) {
const body = await created.json();
if (body.code === 'EXTERNAL_ID_EXISTS') {
agentId = await findAgentByExternalId(business.id); // below
if (!agentId) throw new Error('The agent exists but is not in the list');
} else if (body.code === 'IDEMPOTENCY_IN_PROGRESS') {
// The same request is still being processed — call again shortly with the same signupId.
throw new RetryLater();
} else {
// IDEMPOTENCY_KEY_MISMATCH: the key was reused with a different body. Retrying will not change that.
throw new Error(`Agent creation conflict: ${body.code}`);
}
} else {
throw new Error(`Agent creation failed: ${created.status}`);
}
// 2) Agent settings — always check the result.
const configured = await fetch(`${BASE_URL}/v1/agents/${agentId}/settings`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
persona: business.persona,
greeting: `Hello, this is ${business.name}.`,
}),
});
if (!configured.ok) {
const body = await configured.json();
// If it is a persona.* code, body.error can be shown to the operator as is.
throw new Error(`Saving settings failed: ${configured.status} ${body.code ?? ''}`);
}
// 3) Number connection: send the agentId and region to support@livespeech.ai.
return agentId;
}import os, requests
BASE_URL = os.environ["LS_BASE_URL"]
API_KEY = os.environ["LS_API_KEY"]
class RetryLater(Exception):
"""Raised when the same signup_id should be tried again shortly. The caller catches it and retries."""
# signup_id = your sign-up record's identifier. Reuse it on retries; a new sign-up gets a new value.
def onboard(business, signup_id):
# 1) Create the agent
created = requests.post(
f"{BASE_URL}/v1/agents",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": f"create-{business['id']}-{signup_id}",
},
json={"name": business["name"], "region": "kr", "externalId": business["id"]},
)
if created.status_code == 201:
agent_id = created.json()["agentId"]
elif created.status_code == 409:
code = created.json().get("code")
if code == "EXTERNAL_ID_EXISTS":
agent_id = find_agent_by_external_id(business["id"]) # below
if not agent_id:
raise RuntimeError("The agent exists but is not in the list")
elif code == "IDEMPOTENCY_IN_PROGRESS":
# The same request is still being processed — call again shortly with the same signup_id.
raise RetryLater()
else:
# IDEMPOTENCY_KEY_MISMATCH: the key was reused with a different body. Retrying will not change that.
raise RuntimeError(f"Agent creation conflict: {code}")
else:
raise RuntimeError(f"Agent creation failed: {created.status_code}")
# 2) Agent settings — always check the result.
configured = requests.put(
f"{BASE_URL}/v1/agents/{agent_id}/settings",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"persona": business["persona"],
"greeting": f"Hello, this is {business['name']}.",
},
)
if not configured.ok:
body = configured.json()
# If it is a persona.* code, body["error"] can be shown to the operator as is.
raise RuntimeError(f"Saving settings failed: {configured.status_code} {body.get('code', '')}")
# 3) Number connection: send the agent_id and region to support@livespeech.ai.
return agent_id# SIGNUP_ID = your sign-up record's identifier. Reuse it only when retrying the same request.
SIGNUP_ID="2026-09-09-0001"
EXTERNAL_ID="dental_00123"
# 1) Create the agent — capture the status code together with the body.
RESPONSE=$(curl -s -w '\n%{http_code}' -X POST "$LS_BASE_URL/v1/agents" \
-H "Authorization: Bearer $LS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-$EXTERNAL_ID-$SIGNUP_ID" \
-d "{\"name\":\"Riverside Dental\",\"region\":\"kr\",\"externalId\":\"$EXTERNAL_ID\"}")
STATUS=$(printf '%s' "$RESPONSE" | tail -n1)
BODY=$(printf '%s' "$RESPONSE" | sed '$d')
# On 201 use the agentId from the response; on 409 EXTERNAL_ID_EXISTS use the one found in the list.
case "$STATUS" in
201)
AGENT_ID=$(printf '%s' "$BODY" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["agentId"])') ;;
409)
CODE=$(printf '%s' "$BODY" \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("code", ""))')
if [ "$CODE" = "EXTERNAL_ID_EXISTS" ]; then
# Looks within the first 200 agents. With more agents, page through like the Node and Python examples.
AGENT_ID=$(curl -s "$LS_BASE_URL/v1/agents?limit=200" \
-H "Authorization: Bearer $LS_API_KEY" \
| python3 -c "import json,sys; print(next(t['agentId'] for t in json.load(sys.stdin)['agents'] if t['externalId'] == '$EXTERNAL_ID'))")
else
# IDEMPOTENCY_IN_PROGRESS: try again shortly with the same SIGNUP_ID. IDEMPOTENCY_KEY_MISMATCH: use a new key.
echo "Agent creation conflict: $CODE" >&2; exit 1
fi ;;
*)
echo "Agent creation failed: $STATUS $BODY" >&2; exit 1 ;;
esac
echo "agentId=$AGENT_ID"
# 2) Agent settings — with the AGENT_ID chosen above; check the status code.
curl -s -o /dev/null -w '%{http_code}\n' \
-X PUT "$LS_BASE_URL/v1/agents/$AGENT_ID/settings" \
-H "Authorization: Bearer $LS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"persona":"You are the business service agent for Riverside Dental. Be polite, brief and friendly.","greeting":"Hello, this is Riverside Dental."}'Branch a 409 on code — EXTERNAL_ID_EXISTS means "the agent exists, find it in the list", IDEMPOTENCY_IN_PROGRESS means "the first request with this key is still running, try again shortly with the same key", and IDEMPOTENCY_KEY_MISMATCH means "this key was used with a different body, so retrying gives the same result". Details are in Errors & limits.
Match by listing
Walk the list once to build an externalId → agentId table, and later lookups can stay inside your own database.
const BASE_URL = process.env.LS_BASE_URL;
const API_KEY = process.env.LS_API_KEY;
async function buildAgentIndex() {
const index = new Map();
let cursor = null;
do {
const url = new URL(`${BASE_URL}/v1/agents`);
url.searchParams.set('limit', '200');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await (
await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } })
).json();
for (const agent of page.agents) {
if (agent.externalId) index.set(agent.externalId, agent.agentId);
}
cursor = page.nextCursor;
} while (cursor);
return index;
}
async function findAgentByExternalId(externalId) {
return (await buildAgentIndex()).get(externalId);
}import os, requests
BASE_URL = os.environ["LS_BASE_URL"]
API_KEY = os.environ["LS_API_KEY"]
def build_agent_index():
index, cursor = {}, None
while True:
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
page = requests.get(
f"{BASE_URL}/v1/agents",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params,
).json()
for agent in page["agents"]:
if agent["externalId"]:
index[agent["externalId"]] = agent["agentId"]
cursor = page.get("nextCursor")
if not cursor:
return index
def find_agent_by_external_id(external_id):
return build_agent_index().get(external_id)curl -s "$LS_BASE_URL/v1/agents?limit=200" \
-H "Authorization: Bearer $LS_API_KEY" \
| python3 -c 'import json,sys; [print(t["externalId"], t["agentId"]) for t in json.load(sys.stdin)["agents"]]'Terminated agents are not in the list. A record of yours with no agent in the list is one whose agent was terminated.
Agent quota
If your account has an agent quota, creation fails with 402 AGENT_QUOTA_EXCEEDED the moment you exceed it. Terminated agents do not count. To raise the quota, email support@livespeech.ai.
Termination and re-onboarding
When a business leaves, terminate its agent. Once termination completes, the externalId is released, so a returning business can be created with the same identifier. A new agentId is issued — update your mapping too.
If a number is still attached, termination is held and you get 502 CARRIER_RELEASE_FAILED — email support@livespeech.ai with the agentId to release the number, then call again.
Collect call results
With many agents, one webhook beats polling every agent's call list. There is one webhook per account, and the payload's agentId tells you which agent it is. Use the list to fill gaps the webhook missed or to show calls on a screen.