Skip to content

Personas

A persona is a named bundle of roles, each linked at a relationship scope. Assigning a persona adds the user to each linked role at that scope; the memberships are ordinary rows in lt_user_roles (the single source of authorization truth), and the persona records why they exist. See Identity for provenance, the highest-allowance union and overlay-on-reassign.

All endpoints require authentication plus role-management access (admin type, superadmin, or the engineer role), the same audience that manages users and roles.

Relationship values map onto the member scope lattice (see Work-surface scope):

relationship read_scope write_scope
write-all all all
write-self all self
read-all all none

write-none is accepted anywhere as a synonym for read-all.

The recompute object in mutation responses counts the membership changes: granted (memberships inserted), refreshed (persona-sustained rows overlaid), raised (direct rows lifted to the union) and removed (sustained rows no held persona still grants).

GET /api/personas

Returns every persona with its role links and assignee count.

Example request

Terminal window
curl https://lt.acme.dev/api/personas \
-H "Authorization: Bearer $LT_TOKEN"

Response 200

{
"personas": [
{
"id": "a1b2c3d4-...",
"key": "production-manager",
"title": "Production Manager",
"description": "Runs the line: works intake and review, watches packing.",
"roles": [
{ "role": "intake", "relationship": "write-all" },
{ "role": "review", "relationship": "write-all" },
{ "role": "packing", "relationship": "read-all" }
],
"user_count": 2,
"created_at": "2026-07-01T08:00:00.000Z",
"updated_at": "2026-07-01T08:00:00.000Z"
}
]
}
GET /api/personas/:key

Returns one persona and the users who hold it.

Path parameters

Parameter Description
key Persona key

Example request

Terminal window
curl https://lt.acme.dev/api/personas/production-manager \
-H "Authorization: Bearer $LT_TOKEN"

Response 200

The persona record plus assignees: [{ id, external_id, display_name, email, assigned_at }].

Response 404

The key is unknown.

POST /api/personas

Creates a persona with no role links. Link roles with PUT /api/personas/:key/roles/:role.

Request body

Field Type Description
key string Required. Stable identifier in the role-name alphabet (^[a-z][a-z0-9_-]*$)
title string Display title
description string The day-in-the-life, one paragraph

Example request

Terminal window
curl -X POST https://lt.acme.dev/api/personas \
-H "Authorization: Bearer $LT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "key": "production-manager", "title": "Production Manager", "description": "Runs the line: works intake and review, watches packing." }'

Response 201

The created persona record.

Response 409

The key already exists.

PATCH /api/personas/:key

Updates title and description. Omitted fields keep their values; null clears.

Path parameters

Parameter Description
key Persona key

Request body

Field Type Description
title string | null Display title
description string | null The day-in-the-life, one paragraph

Example request

Terminal window
curl -X PATCH https://lt.acme.dev/api/personas/production-manager \
-H "Authorization: Bearer $LT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "title": "Line Manager" }'

Response 200

The updated persona record.

DELETE /api/personas/:key

Removes the memberships the persona sustains (or re-homes them to a sibling persona the user still holds), then deletes the persona row. Direct grants stay as they are.

Path parameters

Parameter Description
key Persona key

Example request

Terminal window
curl -X DELETE https://lt.acme.dev/api/personas/production-manager \
-H "Authorization: Bearer $LT_TOKEN"

Response 200

{ "deleted": true, "recompute": { "granted": 0, "refreshed": 0, "raised": 0, "removed": 3 } }
PUT /api/personas/:key/roles/:role

Links a role to the persona at a relationship, or updates an existing link’s relationship. The role is created if absent, and every current holder’s memberships are reconciled in the same transaction.

Path parameters

Parameter Description
key Persona key
role Role name

Request body

Field Type Description
relationship string write-all, write-self, read-all (or write-none)

Example request

Terminal window
curl -X PUT https://lt.acme.dev/api/personas/production-manager/roles/intake \
-H "Authorization: Bearer $LT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "relationship": "write-all" }'

Response 201

{ "role": "intake", "relationship": "write-all", "recompute": { ... } }
DELETE /api/personas/:key/roles/:role

Removes the link and reconciles every holder.

Path parameters

Parameter Description
key Persona key
role Role name

Example request

Terminal window
curl -X DELETE https://lt.acme.dev/api/personas/production-manager/roles/packing \
-H "Authorization: Bearer $LT_TOKEN"

Response 200

{ "unlinked": true, "recompute": { ... } }
POST /api/personas/seed

Declares personas and their role links in one idempotent call. The spec is authoritative: titles and descriptions are overlaid, role links are synced (links absent from the spec are pruned), linked roles are ensured as foreign-key targets, and every current holder is reconciled. The SDK twin is lt.personas.seed(specs); declare personas in the same seed pass that declares roles and default_pins.

Request body

Field Type Description
personas array [{ key, title?, description?, roles: [{ role, relationship }] }]

Example request

Terminal window
curl -X POST https://lt.acme.dev/api/personas/seed \
-H "Authorization: Bearer $LT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"personas": [
{
"key": "production-manager",
"title": "Production Manager",
"roles": [
{ "role": "intake", "relationship": "write-all" },
{ "role": "packing", "relationship": "read-all" }
]
}
]
}'

Response 200

{ "personas": 2, "links": 6, "recompute": { ... } }
GET /api/users/:id/personas

Returns the personas a user holds and the composed role and scope map.

Path parameters

Parameter Description
id User UUID

Example request

Terminal window
curl https://lt.acme.dev/api/users/b2c3d4e5-.../personas \
-H "Authorization: Bearer $LT_TOKEN"

Response 200

{
"personas": [
{ "id": "a1b2c3d4-...", "key": "production-manager", "title": "Production Manager", "description": "...", "roles": [ ... ], "assigned_at": "2026-07-10T12:00:00.000Z" }
],
"roles": [
{ "role": "intake", "read_scope": "all", "write_scope": "all", "granted_by_persona": "production-manager" },
{ "role": "review", "read_scope": "all", "write_scope": "all", "granted_by_persona": null }
]
}

roles lists every membership with the persona sustaining it (null means a direct grant).

POST /api/users/:id/personas

Assigns a persona to a user. Idempotent: re-assigning overlays fresh from the persona’s current role links. The highest allowance wins across held personas, and a direct grant’s scope is only ever raised toward the union.

Path parameters

Parameter Description
id User UUID

Request body

Field Type Description
persona string Persona key

Example request

Terminal window
curl -X POST https://lt.acme.dev/api/users/b2c3d4e5-.../personas \
-H "Authorization: Bearer $LT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "persona": "production-manager" }'

Response 200

{ "assigned": true, "recompute": { "granted": 3, "refreshed": 0, "raised": 1, "removed": 0 } }
DELETE /api/users/:id/personas/:key

Removes only the memberships the persona sustains; rows another held persona still grants are re-homed to it. Direct grants stay as they are, including a membership added directly on top of a persona grant.

Path parameters

Parameter Description
id User UUID
key Persona key

Example request

Terminal window
curl -X DELETE https://lt.acme.dev/api/users/b2c3d4e5-.../personas/production-manager \
-H "Authorization: Bearer $LT_TOKEN"

Response 200

{ "unassigned": true, "recompute": { ... } }

Response 404

The user does not hold the persona.