Developers · API
v1Create and terminate lines
One customer is one line. This page covers what a line is, then creating, reading and terminating one.
Accounts and lines
An account is the owner of the API key — your company. One account holds many lines.
A line is one customer's AI agent. One phone number, one set of agent settings and one call history belong to a line. When you take on a new customer, create a line.
Account (your company, one API key)
├── Line A ← customer A: number · agent settings · call history
├── Line B ← customer B
└── Line C ← customer C
The key works only on lines delegated to it. Lines you create are delegated automatically; any other line answers 403 without revealing whether it exists.
Two names for a line
The same line appears under two names depending on where it is. The value is the same.
| Name | Where |
|---|---|
tenantId | Create, list and read responses; webhook payloads |
customerId | Path parameter (/v1/tenants/{customerId}/…) |
Attach your own customer identifier to a line as externalId (1–128 printable ASCII characters, no whitespace). Set it once when you create the line and it comes back in every list response, so you can match lines to customers without a separate mapping table. Two lines cannot share an externalId (409 EXTERNAL_ID_EXISTS).
Region
Choose region when you create a line. It is the service region, and it does not change unless you create the line again.
| Value | Region |
|---|---|
kr | Korea |
ca | Canada |
Line status
status | Meaning |
|---|---|
active | Normal — the line answers calls and its settings can be changed |
terminated | Terminated — settings are locked and the line leaves the list |
Once termination completes, the line's externalId is released for reuse and the line no longer counts against the line quota. Accessing a terminated line answers 403.
Create a line
POST /v1/tenants — send a display name, the region (kr or ca) and your customer identifier (externalId). To set the agent up from the start, include agent (Agent settings). Add an Idempotency-Key header so retries are safe (API reference).
curl -s -X POST "$LS_BASE_URL/v1/tenants" \
-H "Authorization: Bearer $LS_PARTNER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-mall-00123-1" \
-d '{
"name": "Example Mall",
"region": "kr",
"externalId": "mall_00123",
"agent": {
"greeting": "Hello, this is Example Mall."
}
}'const BASE_URL = process.env.LS_BASE_URL;
const API_KEY = process.env.LS_PARTNER_KEY;
const res = await fetch(`${BASE_URL}/v1/tenants`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'create-mall-00123-1',
},
body: JSON.stringify({
name: 'Example Mall',
region: 'kr',
externalId: 'mall_00123',
agent: { greeting: 'Hello, this is Example Mall.' },
}),
});
if (res.status === 409) {
const body = await res.json();
if (body.code === 'EXTERNAL_ID_EXISTS') {
// Already created — find it in the list and use that line.
}
}import os, requests
BASE_URL = os.environ["LS_BASE_URL"]
API_KEY = os.environ["LS_PARTNER_KEY"]
res = requests.post(
f"{BASE_URL}/v1/tenants",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": "create-mall-00123-1",
},
json={
"name": "Example Mall",
"region": "kr",
"externalId": "mall_00123",
"agent": {"greeting": "Hello, this is Example Mall."},
},
)
if res.status_code == 409 and res.json().get("code") == "EXTERNAL_ID_EXISTS":
pass # Already created — find it in the list and use that line.201
{
"tenantId": "8f2c1d40-5b7a-4c31-9e08-1a2b3c4d5e6f",
"name": "Example Mall",
"region": "kr",
"status": "active",
"externalId": "mall_00123",
"createdAt": "2026-09-09T04:15:22.140Z"
}
The create response has no phone number. Email support@livespeech.ai with the tenantId and region and we connect one; once connected, the number shows in the read response's number.
Every request and response field, including length and format limits, is in the API reference; failure responses are in Errors & limits.
List lines
GET /v1/tenants — limit sets the page size (1–200, default 50); pass the previous response's nextCursor as cursor to read the next page.
curl -s "$LS_BASE_URL/v1/tenants?limit=200" \
-H "Authorization: Bearer $LS_PARTNER_KEY"
# Next page: pass the previous response's nextCursor as cursor.
curl -s "$LS_BASE_URL/v1/tenants?limit=200&cursor=<nextCursor from the previous response>" \
-H "Authorization: Bearer $LS_PARTNER_KEY"const BASE_URL = process.env.LS_BASE_URL;
const API_KEY = process.env.LS_PARTNER_KEY;
async function listAllTenants() {
const all = [];
let cursor = null;
do {
const url = new URL(`${BASE_URL}/v1/tenants`);
url.searchParams.set('limit', '200');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const page = await res.json();
all.push(...page.tenants);
cursor = page.nextCursor;
} while (cursor);
return all;
}import os, requests
BASE_URL = os.environ["LS_BASE_URL"]
API_KEY = os.environ["LS_PARTNER_KEY"]
def list_all_tenants():
all_tenants, cursor = [], None
while True:
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
page = requests.get(
f"{BASE_URL}/v1/tenants",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params,
).json()
all_tenants += page["tenants"]
cursor = page.get("nextCursor")
if not cursor:
return all_tenants{
"tenants": [
{
"tenantId": "8f2c1d40-5b7a-4c31-9e08-1a2b3c4d5e6f",
"name": "Example Mall",
"region": "kr",
"status": "active",
"externalId": "mall_00123",
"createdAt": "2026-09-09T04:15:22.140Z"
}
],
"nextCursor": null
}
nextCursor of null means the last page. Pass cursor values through unchanged — an edited cursor answers 400 INVALID_CURSOR. Every field is in the API reference.
Read a line
GET /v1/tenants/{customerId} — on top of the list fields, the response carries the number the line is currently serving (number, null before one is connected), commerce integration status (integrations) and a plan summary (plan).
TENANT_ID="<customerId from the create response>"
curl -s "$LS_BASE_URL/v1/tenants/$TENANT_ID" \
-H "Authorization: Bearer $LS_PARTNER_KEY"const BASE_URL = process.env.LS_BASE_URL;
const API_KEY = process.env.LS_PARTNER_KEY;
const tenantId = '<customerId from the create response>';
const res = await fetch(`${BASE_URL}/v1/tenants/${tenantId}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const tenant = await res.json();
console.log(tenant.number ?? 'no number connected yet');import os, requests
BASE_URL = os.environ["LS_BASE_URL"]
API_KEY = os.environ["LS_PARTNER_KEY"]
tenant_id = "<customerId from the create response>"
tenant = requests.get(
f"{BASE_URL}/v1/tenants/{tenant_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
).json()
print(tenant["number"] or "no number connected yet"){
"tenantId": "8f2c1d40-5b7a-4c31-9e08-1a2b3c4d5e6f",
"name": "Example Mall",
"region": "kr",
"status": "active",
"externalId": "mall_00123",
"createdAt": "2026-09-09T04:15:22.140Z",
"number": "07012345678",
"integrations": {
"cafe24": { "connected": true },
"nextengine": { "connected": false }
},
"plan": { "tier": "basic", "status": "active" }
}
Every field is in the API reference.
Terminate a line
DELETE /v1/tenants/{customerId} — both body fields are optional. reason records why; purge: true also removes what the line stored, such as the persona and summary recipients (default false).
TENANT_ID="<customerId from the create response>"
curl -s -X DELETE "$LS_BASE_URL/v1/tenants/$TENANT_ID" \
-H "Authorization: Bearer $LS_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer closed their account", "purge": true }'const BASE_URL = process.env.LS_BASE_URL;
const API_KEY = process.env.LS_PARTNER_KEY;
const tenantId = '<customerId from the create response>';
const res = await fetch(`${BASE_URL}/v1/tenants/${tenantId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ reason: 'Customer closed their account', purge: true }),
});
if (res.status === 502) {
// The number is still attached — email support@livespeech.ai with the tenantId to release the number, then call again.
}import os, requests
BASE_URL = os.environ["LS_BASE_URL"]
API_KEY = os.environ["LS_PARTNER_KEY"]
tenant_id = "<customerId from the create response>"
res = requests.delete(
f"{BASE_URL}/v1/tenants/{tenant_id}",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={"reason": "Customer closed their account", "purge": True},
)
if res.status_code == 502:
pass # The number is still attached — email support@livespeech.ai with the tenantId to release the number, then call again.{
"status": "terminated",
"numberReclaimed": false,
"terminatedAt": "2026-09-09T05:02:11.885Z"
}
When termination completes:
externalIdis released, so a line can be created again with the same value.- The line no longer counts against the line quota.
- It disappears from list and read; later access answers
403.
If a number is still attached, termination is held and you get 502 CARRIER_RELEASE_FAILED — the line stays active, so email support@livespeech.ai with the tenantId to release the number, then call again. Sending DELETE again gets one of two answers. If the earlier termination recorded the status but had not finished releasing the delegation, you get 409 ALREADY_TERMINATED and the release runs again — nothing more to do. Once termination is complete the line is no longer delegated to you, so a repeat DELETE (like any other request on that line) answers 403 — treat it as already done.
Every field is in the API reference; failure responses are in Errors & limits.