View as Markdown

Groups and modules

v1 has two roles, admin and user. An admin sees everything in the tenant and is in no group. What a user can see and do comes from the user's groups: a group is a set of the tenant's modules, each at the level read or read_write. A user can be in several groups, and the rights add up; the higher level wins. Aleta ships two standard groups, Viewer (every module at read) and Editor (every module at read_write), both without the audit module.

Every v1 route belongs to a module. A read needs the module at read, a write at read_write (a tag preview writes nothing and needs read); a few routes (most platform writes, webhook subscriptions) are for admins only, and a user gets 403 forbidden there. The scopes of your token or API key stay the ceiling: a module never opens a route your scopes do not. Access administration (users, invites, API keys, access grants, groups) is no module: only an admin changes it.

GET/v1/permissions

Modules

The module catalog: what a group can hold. Every caller with access to the tenant can read it, without any scope. A module that can only be read lists only read in levels.

keystring

core (clients, entities, cash and custody accounts, holdings, performance, lenses, tags), market_data, transactions, registrations, limits, benchmarks, risk, private_markets, documents, reports, operations (reconciliation, imports, jobs, events, data status), audit (read only), esg, wealth_map or allocation.

descriptionstring

What the module covers.

levelsstring[]

["read", "read_write"], or ["read"].

Response
{
  "data": [
    {
      "key": "core",
      "description": "Clients, entities, cash and custody accounts, holdings, performance, lenses and tags",
      "levels": ["read", "read_write"]
    },
    {
      "key": "audit",
      "description": "Audit events: who did what, and when",
      "levels": ["read"]
    }
  ],
  "next_cursor": null,
  "limit": 15
}

GET/v1/tenants/current

Your tenant

The tenant you call, one object. Needs platform:read.

id, name, region, status, plan, created_atstring

The tenant. region is the region its data lives in, as region_id on GET /v1/residency (for example aws-eu-central-1). status, plan and created_at can be null.

modulesstring[]

The modules the tenant has; a group can only hold these.

flagsobject

What Aleta has sold the tenant: ai_enabled, cube_enabled, api_enabled, mcp_enabled. api_enabled and mcp_enabled say sold, not whether a call works. ai_enabled is true unless AI is switched off for the tenant.

white_label_levelstring

simple, standard or extended.

Response
{
  "id": "3f600564-957b-4aab-96ed-fd0118cfd805",
  "name": "development-client-testing-only",
  "region": "aws-eu-central-1",
  "status": "active",
  "plan": null,
  "created_at": "2026-07-01T09:00:00Z",
  "modules": ["core", "market_data", "transactions", "registrations", "limits", "benchmarks", "risk", "private_markets", "documents", "reports", "operations", "audit", "esg", "wealth_map", "allocation"],
  "flags": { "ai_enabled": true, "cube_enabled": false, "api_enabled": true, "mcp_enabled": false },
  "white_label_level": "simple"
}

The group

idstring

Stable identifier. A UUID.

namestring

Unique among the groups in use, in any case.

kindstring

standard (Viewer and Editor: the name stays and they are never archived) or custom.

modulesobject[]

{module, level} per module, in catalog order.

member_countinteger

The users in the group now.

membersobject[]

{user_id, added_at, origin} per user, on a single group and on a write's answer. Not in a list.

originstring

legacy or v1. A legacy group is read-only until the tenant has moved (409 legacy_managed).

created_by_user_id, created_at, updated_atstring

Who made the group, and when. created_by_user_id is null on the standard groups and on legacy groups.

archived_at, archived_by, archive_reasonstring or null

Set while the group is archived. An archived group gives no rights; its members stay in it.


GET/v1/groups

List groups

An admin with access:read gets every group; anyone else gets the groups they are in. Pages in id order. An unknown query parameter is a 400.

include_archivedboolean

Archived groups are left out unless true.

limit, cursorinteger, string

Page size (default 100, at most 1 000) and the previous page's next_cursor.

Response
{
  "data": [
    {
      "id": "019e9a11-2b6c-7d3e-8f10-6a2b3c4d5e6f",
      "name": "Viewer",
      "kind": "standard",
      "modules": [{ "module": "core", "level": "read" }],
      "member_count": 4,
      "origin": "v1",
      "created_by_user_id": null,
      "created_at": "2026-10-16T20:00:00.000000Z",
      "updated_at": "2026-10-16T20:00:00.000000Z",
      "archived_at": null,
      "archived_by": null,
      "archive_reason": null
    }
  ],
  "next_cursor": null,
  "limit": 100
}
GET/v1/groups/{id}

Get a group

The group with its members. 404 for a group you are not in, unless you are an admin with access:read.


Change groups

Only an admin changes groups: a person, or an API key an admin owns (Aleta's fleet key too); the token needs access:write, and anyone else gets 403 forbidden. Every change takes ?dry_run=true and Idempotency-Key (see Writes) and is logged once. Times are RFC 3339 UTC with microseconds. An unknown body field is a 400; an {id} that is not a UUID, or a group that does not exist, is a 404. A legacy group answers 409 legacy_managed until the tenant has moved. Every 409 names its rule in code; the group rules below also carry next_action (null when there is no step to take; for an archived group the restore call, PATCH /v1/groups/{id} with status).

POST/v1/groups

Create a group

201 with the group, its members included (an empty list).

namestring

Trimmed; must not be empty and holds at most 100 characters (400). 409 name_taken when a group in use has the name, in any case.

modulesobject[]

Optional: [{module, level}], level one of read or read_write. 400 for an unknown module, access, a level the module does not allow (audit is only read) or a module named twice. Every tenant has every module.

cURL
curl -X POST https://api.aleta.ai/v1/groups       -H "Authorization: Bearer {access_token}"       -H "Idempotency-Key: {uuid}"       -H "Content-Type: application/json"       -d '{"name": "Analysts", "modules": [{"module": "core", "level": "read"}, {"module": "reports", "level": "read_write"}]}'
PATCH/v1/groups/{id}

Update a group

Renames the group or brings an archived group back. 200 with the group and its members.

namestring

The new name, by the same rules as on create. 409 standard_group for a new name on a standard group.

statusstring

active brings back an archived group. archived is a 400: archive with DELETE.

At least one of the two (400 otherwise). 409 name_taken when the new name, or the name of a group brought back, is in use.

cURL
curl -X PATCH https://api.aleta.ai/v1/groups/{id}       -H "Authorization: Bearer {access_token}"       -H "Idempotency-Key: {uuid}"       -H "Content-Type: application/json"       -d '{"status": "active"}'
DELETE/v1/groups/{id}

Archive a group

Archives the group: 204, also when it is archived already. Its rights end within 5 seconds; its members stay. ?reason= is kept as archive_reason. Any query parameter other than reason and dry_run is a 400. 409 standard_group for a standard group.

cURL
curl -X DELETE "https://api.aleta.ai/v1/groups/{id}?reason=team%20disbanded"       -H "Authorization: Bearer {access_token}"       -H "Idempotency-Key: {uuid}"
PUT/v1/groups/{id}/modules

Set the modules

Body {modules}: the whole list as [{module, level}], replacing the stored one; modules is required. 200 with the group. 400 for an unknown module, access, a level the module does not allow (audit is only read) or a module named twice; 409 in_use for an archived group (bring it back with PATCH status: active first).

cURL
curl -X PUT https://api.aleta.ai/v1/groups/{id}/modules       -H "Authorization: Bearer {access_token}"       -H "Idempotency-Key: {uuid}"       -H "Content-Type: application/json"       -d '{"modules": [{"module": "core", "level": "read_write"}, {"module": "audit", "level": "read"}]}'
POST/v1/groups/{id}/members

Add a member

Body {user_id}, a UUID (400 otherwise). 201 with the group and its members. The group is checked before the person: 404, 409 legacy_managed, 409 in_use for an archived group. Then the person: 404 for anyone GET /v1/users/{id} does not find (Aleta staff, API keys, users without access to the tenant) and 409 admin_in_no_group for an admin (admins are in no group); 409 already_member for a user in the group already.

cURL
curl -X POST https://api.aleta.ai/v1/groups/{id}/members       -H "Authorization: Bearer {access_token}"       -H "Idempotency-Key: {uuid}"       -H "Content-Type: application/json"       -d '{"user_id": "019e9a40-1a2b-7c3d-8e4f-5a6b7c8d9e0f"}'
DELETE/v1/groups/{id}/members/{user_id}

Remove a member

Takes the user out: 204, also on an archived group. 404 when the user is not in the group or either id is not a UUID. A membership from the legacy platform answers 409 legacy_managed until the tenant has moved. A replay under the same Idempotency-Key after the user left answers the first 204.

cURL
curl -X DELETE https://api.aleta.ai/v1/groups/{id}/members/{user_id}       -H "Authorization: Bearer {access_token}"       -H "Idempotency-Key: {uuid}"