API reference

Team and access tokens API

Your team is the people who work in the workspace. Each member has a role (built-in or custom) that sets what they can do, the creators they work on, and a personal access key to sign in. Access tokens are the keys of the API and of the MCP connection. All these endpoints need an owner token: in the examples, $LANACHAT_TOKEN holds one.

Planned

Every endpoint on this page is marked Planned: it is the published contract the backend is built from, and it does not answer yet.

Endpoints #

MethodPathDescriptionPermissionStatus
GET/membersList members (status=active,disabled, role_id=, q=)ownerPlanned
POST/membersAdd a member with a role and creatorsownerPlanned
GET/members/{id}One member, last activity and overridesownerPlanned
PATCH/members/{id}Change role, creators, overrides; disable or enableownerPlanned
DELETE/members/{id}Delete a member (their past messages keep their name)ownerPlanned
POST/members/{id}/access-keyCreate a personal access key (shown once)ownerPlanned
POST/members/{id}/invitationE-mail an access key to the memberownerPlanned
DELETE/members/{id}/access-keyRevoke the member's key: they are signed outownerPlanned
GET/rolesBuilt-in and custom roles with their permission tableownerPlanned
POST/rolesCreate a custom roleownerPlanned
PATCH/roles/{id}Rename a role or change its permissionsownerPlanned
DELETE/roles/{id}Delete a custom role no member hasownerPlanned
GET/tokensAPI and MCP access tokens (include_revoked=true)ownerPlanned
POST/tokensCreate a token with its permissions (shown once)ownerPlanned
PATCH/tokens/{id}Rename a token or change its permissionsownerPlanned
POST/tokens/{id}/rotateReplace the secret; the old one stops at onceownerPlanned
DELETE/tokens/{id}Revoke a tokenownerPlanned

The member object #

JSON
{
  "id": "mem_4",
  "name": "Ana",
  "email": "[email protected]",
  "status": "active",
  "role_id": "role_chatter",
  "creator_ids": ["cr_lea", "cr_mia"],
  "overrides": { "billing.read": false, "fans.export": true },
  "access_key": { "active": true, "created_at": "2026-09-20T09:00:00Z" },
  "last_active_at": "2026-10-06T13:40:00Z"
}
FieldTypeDescription
idstringStarts with mem_
namestringTheir name in the Console
emailstring or nullWhere the invitation goes
statusstringactive or disabled
role_idstringTheir role
creator_idsarray of stringsThe creators they work on
overridesobjectPermissions set for this member only, on top of the role
access_keyobjectWhether they have an active personal access key, and since when; never the key itself
last_active_atstring or nullTheir last activity

A disabled member cannot sign in and keeps their history, role and creators.

The role object #

JSON
{
  "id": "role_chatter",
  "name": "Chatter",
  "builtin": true,
  "permissions": { "live.read": true, "live.reply": true, "fans.read": true, "fans.export": false, "billing.read": false },
  "members": 6
}
FieldTypeDescription
idstringThe role
namestringIts name
builtinbooleantrue for Owner, Manager and Chatter
permissionsobjectOne key per row of the permission table, true or false
membersintegerHow many members have it

Permission keys follow the rows of the permission table in Team Management › Roles, grouped by area: Dashboard, Creators, AI Brain, Conversations, Vault, Scripts, Playground, Telegram and Employees. Owner always has every permission and cannot be edited. Manager and Chatter are built in: you can change their permissions but not rename or delete them.

The access token object #

JSON
{
  "id": "tok_31",
  "name": "Reporting script",
  "permissions": ["can_fan_read", "can_sales_read"],
  "status": "active",
  "created_at": "2026-10-06T15:10:00Z",
  "expires_at": "2027-01-04T15:10:00Z"
}
FieldTypeDescription
idstringStarts with tok_
namestringUp to 100 characters, required
permissionsarray of stringsThe permissions it has
statusstringActive, expired or revoked
created_at, expires_atstringISO 8601, UTC
tokenstringThe secret, only in the answer that creates or rotates it

Members #

List members #

GET/membersPlannedNeeds owner
ParameterTypeRequiredDescription
statusstringNoactive or disabled; both separated by a comma
role_idstringNoOnly members with this role
qstringNoPart of a name
limit, cursornumber, stringNoPagination
curl "https://api.lanachat.ai/v1/members?status=active&role_id=role_chatter" \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/members?status=active&role_id=role_chatter', {
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.get(
    "https://api.lanachat.ai/v1/members?status=active&role_id=role_chatter",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())
Response
{
  "data": [
    { "id": "mem_4", "name": "Ana", "email": "[email protected]", "status": "active", "role_id": "role_chatter", "creator_ids": ["cr_lea", "cr_mia"], "overrides": {}, "access_key": { "active": true, "created_at": "2026-09-20T09:00:00Z" }, "last_active_at": "2026-10-06T13:40:00Z" }
  ],
  "next_cursor": null
}

Add a member #

POST/membersPlannedNeeds owner
ParameterTypeRequiredDescription
namestringYesTheir name
emailstringNoNeeded to send an invitation
role_idstringYesA built-in or custom role
creator_idsarray of stringsNoThe creators they work on
curl -X POST https://api.lanachat.ai/v1/members \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ana", "email": "[email protected]", "role_id": "role_chatter", "creator_ids": ["cr_lea"]}'
const res = await fetch('https://api.lanachat.ai/v1/members', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Ana',
    email: '[email protected]',
    role_id: 'role_chatter',
    creator_ids: ['cr_lea'],
  }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import uuid
import requests

r = requests.post(
    "https://api.lanachat.ai/v1/members",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Ana",
        "email": "[email protected]",
        "role_id": "role_chatter",
        "creator_ids": ["cr_lea"],
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The answer is the new member (201), active, without an access key yet.

Get a member #

GET/members/{id}PlannedNeeds owner
curl https://api.lanachat.ai/v1/members/mem_4 \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/members/mem_4', {
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.get(
    "https://api.lanachat.ai/v1/members/mem_4",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The answer is the member object. An unknown id answers 404.

Update a member #

PATCH/members/{id}PlannedNeeds owner
ParameterTypeRequiredDescription
role_idstringNoNew role
creator_idsarray of stringsNoReplaces their list of creators
overridesobjectNoPermissions for this member only
statusstringNoactive or disabled
curl -X PATCH https://api.lanachat.ai/v1/members/mem_4 \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"creator_ids": ["cr_lea", "cr_mia"], "overrides": {"fans.export": true}}'
const res = await fetch('https://api.lanachat.ai/v1/members/mem_4', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ creator_ids: ['cr_lea', 'cr_mia'], overrides: { 'fans.export': true } }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.patch(
    "https://api.lanachat.ai/v1/members/mem_4",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    json={"creator_ids": ["cr_lea", "cr_mia"], "overrides": {"fans.export": True}},
    timeout=30,
)
r.raise_for_status()
print(r.json())

The answer is the updated member. status: "disabled" cuts access at once; the role and creators are kept.

Delete a member #

DELETE/members/{id}PlannedNeeds owner
curl -X DELETE https://api.lanachat.ai/v1/members/mem_4 \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/members/mem_4', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.delete(
    "https://api.lanachat.ai/v1/members/mem_4",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The member is removed and their creators are unassigned. Their past messages keep their name. To keep them for later, disable them instead.

Personal access keys #

Team members don't use a password: they sign in with a personal access key. Keys start with emp_.

Create an access key #

POST/members/{id}/access-keyPlannedNeeds owner
curl -X POST https://api.lanachat.ai/v1/members/mem_4/access-key \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const res = await fetch('https://api.lanachat.ai/v1/members/mem_4/access-key', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import uuid
import requests

r = requests.post(
    "https://api.lanachat.ai/v1/members/mem_4/access-key",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())
Response
{ "member_id": "mem_4", "sign_in_url": "https://app.lanachat.ai/login/?mode=employee", "access_key": "emp_6Yq2…", "shown_once": true }

Send both the sign-in address and the key to the person through a channel you trust. A disabled member answers 409: enable them first.

E-mail an access key #

POST/members/{id}/invitationPlannedNeeds owner
curl -X POST https://api.lanachat.ai/v1/members/mem_4/invitation \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const res = await fetch('https://api.lanachat.ai/v1/members/mem_4/invitation', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import uuid
import requests

r = requests.post(
    "https://api.lanachat.ai/v1/members/mem_4/invitation",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The key is e-mailed to the address on their card. A member without an e-mail address, or a disabled one, answers 409.

Revoke an access key #

DELETE/members/{id}/access-keyPlannedNeeds owner
curl -X DELETE https://api.lanachat.ai/v1/members/mem_4/access-key \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/members/mem_4/access-key', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.delete(
    "https://api.lanachat.ai/v1/members/mem_4/access-key",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The key stops at once and the member is signed out. Create a new key to let them back in.

Roles #

List roles #

GET/rolesPlannedNeeds owner
curl https://api.lanachat.ai/v1/roles \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/roles', {
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.get(
    "https://api.lanachat.ai/v1/roles",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

A list of role objects: Owner, Manager, Chatter and your custom roles.

Create a custom role #

POST/rolesPlannedNeeds owner
ParameterTypeRequiredDescription
namestringYesThe role's name
permissionsobjectNoThe rows to turn on; the others are off
curl -X POST https://api.lanachat.ai/v1/roles \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Night chatter", "permissions": {"live.read": true, "live.reply": true, "fans.read": true}}'
const res = await fetch('https://api.lanachat.ai/v1/roles', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Night chatter',
    permissions: { 'live.read': true, 'live.reply': true, 'fans.read': true },
  }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import uuid
import requests

r = requests.post(
    "https://api.lanachat.ai/v1/roles",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Night chatter",
        "permissions": {"live.read": True, "live.reply": True, "fans.read": True},
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The answer is the new role (201) with builtin: false.

Update a role #

PATCH/roles/{id}PlannedNeeds owner
ParameterTypeRequiredDescription
namestringNoNew name (custom roles only)
permissionsobjectNoThe rows to change; the others are kept
curl -X PATCH https://api.lanachat.ai/v1/roles/role_chatter \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"permissions": {"fans.export": false}}'
const res = await fetch('https://api.lanachat.ai/v1/roles/role_chatter', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ permissions: { 'fans.export': false } }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.patch(
    "https://api.lanachat.ai/v1/roles/role_chatter",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    json={"permissions": {"fans.export": False}},
    timeout=30,
)
r.raise_for_status()
print(r.json())

The change applies at once to every member with that role, except their own overrides. Editing Owner, or renaming Manager or Chatter, answers 409.

Delete a custom role #

DELETE/roles/{id}PlannedNeeds owner
curl -X DELETE https://api.lanachat.ai/v1/roles/role_night \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/roles/role_night', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.delete(
    "https://api.lanachat.ai/v1/roles/role_night",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

Deleting a role that members still have answers 409: give them another role first. Built-in roles cannot be deleted.

Access tokens #

List tokens #

GET/tokensPlannedNeeds owner
ParameterTypeRequiredDescription
include_revokedbooleanNotrue also lists revoked tokens
curl "https://api.lanachat.ai/v1/tokens?include_revoked=true" \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/tokens?include_revoked=true', {
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.get(
    "https://api.lanachat.ai/v1/tokens?include_revoked=true",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

A list of access token objects, without their secret.

Create a token #

POST/tokensPlannedNeeds owner
ParameterTypeRequiredDescription
namestringYesWhat it is for, up to 100 characters
permissionsarray of stringsYesThe permissions it gets
expires_in_daysintegerNoWhen it stops
curl -X POST https://api.lanachat.ai/v1/tokens \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Reporting script", "permissions": ["can_fan_read", "can_sales_read"], "expires_in_days": 90}'
const res = await fetch('https://api.lanachat.ai/v1/tokens', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Reporting script',
    permissions: ['can_fan_read', 'can_sales_read'],
    expires_in_days: 90,
  }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import uuid
import requests

r = requests.post(
    "https://api.lanachat.ai/v1/tokens",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Reporting script",
        "permissions": ["can_fan_read", "can_sales_read"],
        "expires_in_days": 90,
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())
Response
{ "id": "tok_31", "name": "Reporting script", "token": "lc_live_xxxxxxxxxxxxxxxx", "permissions": ["can_fan_read", "can_sales_read"], "expires_at": "2027-01-04T15:10:00Z" }

token is in this answer only. A token can never give more than the owner token that created it. A missing name answers 400.

Update a token #

PATCH/tokens/{id}PlannedNeeds owner
ParameterTypeRequiredDescription
namestringNoNew name
permissionsarray of stringsNoReplaces its permissions
curl -X PATCH https://api.lanachat.ai/v1/tokens/tok_31 \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"permissions": ["can_sales_read"]}'
const res = await fetch('https://api.lanachat.ai/v1/tokens/tok_31', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ permissions: ['can_sales_read'] }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.patch(
    "https://api.lanachat.ai/v1/tokens/tok_31",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    json={"permissions": ["can_sales_read"]},
    timeout=30,
)
r.raise_for_status()
print(r.json())

The answer is the token, without its secret. Turning can_engine off also removes publishing, Advanced AI and experiment-report permissions. A revoked token answers 409.

Rotate a token #

POST/tokens/{id}/rotatePlannedNeeds owner
curl -X POST https://api.lanachat.ai/v1/tokens/tok_31/rotate \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const res = await fetch('https://api.lanachat.ai/v1/tokens/tok_31/rotate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import uuid
import requests

r = requests.post(
    "https://api.lanachat.ai/v1/tokens/tok_31/rotate",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The answer is the token with a new token secret, shown once. The old secret stops at once, so update your script right after. A revoked token answers 409.

Revoke a token #

DELETE/tokens/{id}PlannedNeeds owner
curl -X DELETE https://api.lanachat.ai/v1/tokens/tok_31 \
  -H "Authorization: Bearer $LANACHAT_TOKEN"
const res = await fetch('https://api.lanachat.ai/v1/tokens/tok_31', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.LANACHAT_TOKEN}`,
  },
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(await res.json());
import os
import requests

r = requests.delete(
    "https://api.lanachat.ai/v1/tokens/tok_31",
    headers={
        "Authorization": f"Bearer {os.environ['LANACHAT_TOKEN']}",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())

The token stops at once. A token already revoked answers 409.

Warning

A token works like a password. Revoke it as soon as you stop using it or think it leaked.

Errors #

Every endpoint answers errors in the same JSON body, {"error": {"code": "…", "message": "…"}}, with the common codes 400, 401, 402, 403, 404, 409, 429 and 500 listed in Errors. What the endpoints of this page say they answer:

EndpointStatusWhen
Get a member404An unknown id answers 404.
Create an access key409A disabled member answers 409: enable them first.
E-mail an access key409A member without an e-mail address, or a disabled one, answers 409.
Update a role409Editing Owner, or renaming Manager or Chatter, answers 409.
Delete a custom role409Deleting a role that members still have answers 409: give them another role first.
Create a token400A missing name answers 400.
Update a token409A revoked token answers 409.
Rotate a token409A revoked token answers 409.
Revoke a token409A token already revoked answers 409.

Tutorial: API: team members, roles and access tokens