Skip to content
Start free

Developers · API

v1

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

NameWhere
tenantIdCreate, list and read responses; webhook payloads
customerIdPath 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.

ValueRegion
krKorea
caCanada

Line status

statusMeaning
activeNormal — the line answers calls and its settings can be changed
terminatedTerminated — 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/tenantslimit 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:

  • externalId is 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.