View as Markdown

Lenses

Where entities capture who owns the assets, lenses capture how the client wants to look at them. Everything is seen through a lens: a lens is a named view on the client's cash accounts and custody accounts, built from rules, and the scope you pass to holdings and performance. Lenses form a tree: a lens under another lens holds only what its parent holds. A lens never gives access; a user sees what the lens holds within their own access. Reads need reporting:read, writes reporting:write.

Data model

idstring

Stable identifier for the lens. A UUID — treat it as opaque.

client_idstring

The client this view belongs to. Set at creation, immutable afterwards. A lens under another lens takes its parent's client.

parent_idstring | null

The lens this one sits under, null at the top.

namestring

Display name shown in the product.

show_as_portfolioboolean

The lens is shown as a portfolio of its parent. Only a lens with a parent_id can be one. A lens synced from the legacy platform that is a legal entity is shown as a portfolio of its client's structure lens once the client has one; until then it is an ordinary lens at the top.

orderinteger | null

The portfolio's place among its siblings, lowest first; null comes last.

start_datestring | null

YYYY-MM-DD, the first day the lens shows. Holdings, performance and members read with this lens show nothing before it, and a window that starts earlier starts on it. null shows all history.

lockedboolean

The client's structure lens, made with the client and named after it (a new client name renames it): the rule include client, shared with everyone who sees the client. It is never changed, shared otherwise or deleted (409 locked), but portfolios can be put under it. It goes with the client.

currency_codestring | null

ISO 4217. Set on the lens, else the base currency of the entities the lens holds, and the client's base currency when they have more than one or the lens holds none.

accounting_year_startstring

MM-DD, the first day of the accounting year, from the structure the lens sits in: a new lens and a moved one take their parent's. 01-01 when none is set.

rulesarray of object

What the lens holds: mode (include or exclude), kind and ref (the id of what it names). See Rules. A lens synced from the legacy platform shows its accounts as include rules.

originstring

legacy for a lens synced from the legacy platform (read-only until your move to v1 is done, 409 legacy_managed), v1 for one made here.

created_atstring

RFC 3339, UTC.

updated_atstring

RFC 3339, UTC.

statusstring

active, or archived for a lens you deleted (it comes back with PATCH status: active).

archived_atstring | null

When the lens was archived, RFC 3339 UTC; null while it is active.

archived_bystring | null

The user who archived it; null while it is active.

archive_reasonstring | null

Always null for a lens.

Rules

What a lens holds is worked out from its rules every time it is read, so an account opened later, an ownership change or a changed lens a rule names is in the next read by itself.

kindrefholds
clientclient idevery account of the client
entityentity idthe entity's accounts, and its share of every entity it owns, down the ownership chain
partyparty idthe party's share of every entity it owns, directly or through other entities: 40 % owned shows 40 %
ownership_pathownership id (/v1/ownerships)the owner's share along that one ownership, and on down through what the owned entity owns
custody_accountcustody account idthat account
cash_accountcash account idthat account
custodiancustodian idthe client's custody accounts at that custodian
assetasset idthe holdings of that asset
tagtag id (/v1/tags)the holdings of the assets with that tag
lenslens idwhat another lens of the client holds; a loop holds nothing

Include rules add up, to at most the whole account; an exclude takes its share off again, so an exclude that names something whole always wins. A share follows the ownership on each day, so a read over a period counts each day at that day's share. Every ref must be something of the lens's client that you can see (404 otherwise). No rule holds the accounts of a portfolio the legacy platform deleted or that moved to another tenant.

A lens under another lens holds at most what the nearest lens above it with content holds, and a lens without include rules holds what that lens holds (less its exclude rules). A lens without include rules under no lens with content is a folder: it holds what its sub-lenses hold together and limits none of them.

A lens synced from the legacy platform keeps the legacy platform's numbers until your lenses are moved: its accounts, and a holding in two of its groups counted once per group. A lens made in v1 counts each holding once.


GET/v1/lenses

List lenses

Returns the lenses visible to the authenticated credential in the standard v1 list envelope, oldest first; with parent_id, in order (unset last), then oldest first. By default the list holds the active lenses that are not shown as portfolios; pass show_as_portfolio=true for the portfolios, and parent_id for the lenses under one lens. A parameter not listed here is 400. Lenses are not dated, so the list takes no as_of.

Query parameters

client_idstring

Only the lenses of this client.

parent_idstring

Only the lenses directly under this lens.

show_as_portfolioboolean

true lists the lenses shown as portfolios, false (the default) the others.

include_archivedboolean

true lists archived lenses too. Defaults to false.

limitinteger

Page size. Defaults to 100, capped at 1 000.

cursorstring

Opaque cursor from the previous page's next_cursor. Omit for the first page.

Request
GET/v1/lenses
curl -G https://api.aleta.ai/v1/lenses \
  -H "Authorization: Bearer {access_token}" \
  -d limit=2
Response
{
  "data": [
    {
      "id": "019f91be-1d83-7f0a-bbfc-e64861142e87",
      "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
      "parent_id": null,
      "name": "Seed lens",
      "show_as_portfolio": false,
      "order": null,
      "start_date": null,
      "locked": false,
      "currency_code": "DKK",
      "accounting_year_start": "01-01",
      "rules": [
        { "mode": "include", "kind": "custody_account", "ref": "019dfed3-16d5-7b8c-878c-aca5a2407b9e" }
      ],
      "origin": "v1",
      "created_at": "2026-10-17T09:00:00.000000Z",
      "updated_at": "2026-10-17T09:00:00.000000Z",
      "status": "active",
      "archived_at": null,
      "archived_by": null,
      "archive_reason": null
    }
  ],
  "next_cursor": null,
  "limit": 2
}

POST/v1/lenses

Create a lens

Creates a lens at the top of a client's lens tree, or under another lens with parent_id. The new lens is yours and shared with no one; share it with PUT /v1/lenses/{id}/shares. A rule's ref that is not one of the client's, or that you cannot see, returns a 404 problem; so does a parent you cannot see. show_as_portfolio without parent_id is 400 lens_needs_parent, a top lens without client_id is 400 lens_needs_client, and a client_id other than the parent's is 400 lens_parent_client. A new lens takes its parent's accounting year. A blank name, a negative order or a currency_code that is not three capital letters is a 400; more than 500 rules is 400 too_many_rules, a ref that is not an id 400 invalid_rule; a rule sent twice counts once. Unknown fields are a 400. Takes ?dry_run=true and Idempotency-Key (see Writes). Returns 201 with the created resource.

Body parameters

client_idstring

The client to create the view under. Required at the top; under a parent it is the parent's.

parent_idstring

The lens to create it under. Optional.

namestring

Display name. Required.

show_as_portfolioboolean

Show it as a portfolio of its parent. Defaults to false.

orderinteger

Its place among its siblings, 0 or more. Optional.

start_datestring

YYYY-MM-DD. Optional.

currency_codestring

ISO 4217. Optional.

rulesarray of object

What the lens holds (see Rules), at most 500. Optional; a lens without rules is a folder, or under a lens with content holds what that lens holds.

Request
POST/v1/lenses
curl -X POST https://api.aleta.ai/v1/lenses \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
    "name": "Share of Anna",
    "rules": [
      { "mode": "include", "kind": "party", "ref": "019dfed3-16d5-7b8c-878c-aca5a2407b9e" },
      { "mode": "exclude", "kind": "custodian", "ref": "019dfe11-2a3b-7c4d-8e5f-6a7b8c9d0e1f" }
    ]
  }'
Response
{
  "id": "019f91be-1d83-7f0a-bbfc-e64861142e87",
  "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
  "parent_id": null,
  "name": "Share of Anna",
  "show_as_portfolio": false,
  "order": null,
  "start_date": null,
  "locked": false,
  "currency_code": "DKK",
  "accounting_year_start": "01-01",
  "rules": [
    { "mode": "include", "kind": "party", "ref": "019dfed3-16d5-7b8c-878c-aca5a2407b9e" },
    { "mode": "exclude", "kind": "custodian", "ref": "019dfe11-2a3b-7c4d-8e5f-6a7b8c9d0e1f" }
  ],
  "origin": "v1",
  "created_at": "2026-10-17T09:00:00.000000Z",
  "updated_at": "2026-10-17T09:00:00.000000Z",
  "status": "active",
  "archived_at": null,
  "archived_by": null,
  "archive_reason": null
}

GET/v1/lenses/{id}

Retrieve a lens

Fetch a single lens by id. Returns the bare object, an archived lens included (status: archived, with the rules it had), or a 404 problem when the id doesn't exist for this tenant or the legacy platform deleted the lens.

Request
GET/v1/lenses/{id}
curl https://api.aleta.ai/v1/lenses/019f91be-1d83-7f0a-bbfc-e64861142e87 \
  -H "Authorization: Bearer {access_token}"
Response
{
  "id": "019f91be-1d83-7f0a-bbfc-e64861142e87",
  "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
  "parent_id": null,
  "name": "Seed lens",
  "show_as_portfolio": false,
  "order": null,
  "start_date": null,
  "locked": false,
  "currency_code": "DKK",
  "accounting_year_start": "01-01",
  "rules": [
    { "mode": "include", "kind": "custody_account", "ref": "019dfed3-16d5-7b8c-878c-aca5a2407b9e" }
  ],
  "origin": "v1",
  "created_at": "2026-10-17T09:00:00.000000Z",
  "updated_at": "2026-10-17T09:00:00.000000Z",
  "status": "active",
  "archived_at": null,
  "archived_by": null,
  "archive_reason": null
}

GET/v1/lenses/{id}/members

Lens members

The cash and custody accounts the lens holds on as_of, within what you see, in the standard v1 list envelope with as_of. Only accounts open that day count; before the lens's start_date the list is empty. A lens made in v1 is worked out from its rules that day; a lens synced from the legacy platform lists the accounts of the groups its numbers count. An asset or tag rule picks holdings, not accounts: it keeps the accounts it could hold.

The list follows the lens's rows: the accounts of its sub-lenses first, then of its portfolios in order, then the rest ("everything else"). Within a row, custody accounts come before cash accounts. An account in two sub-lenses is listed once, under the first. 404 when you cannot see the lens or it is archived. Any other query parameter is a 400.

Query parameters

as_ofstring

YYYY-MM-DD. Defaults to today (UTC).

limitinteger

Page size. Defaults to 100, capped at 1 000.

cursorstring

Opaque cursor from the previous page's next_cursor.

Member

kindstring

custody_account or cash_account.

idstring

The account.

namestring | null

The account's name.

entity_idstring

The entity that holds the account.

client_idstring

The account's client.

rowsstring[]

The sub-lenses (and portfolios) of this lens the account shows under, in row order; empty for "everything else".

Request
GET/v1/lenses/{id}/members
curl -G https://api.aleta.ai/v1/lenses/019f91be-1d83-7f0a-bbfc-e64861142e87/members \
  -H "Authorization: Bearer {access_token}" \
  -d as_of=2026-10-17
Response
{
  "data": [
    {
      "kind": "custody_account",
      "id": "019dfed3-16d5-7b8c-878c-aca5a2407b9e",
      "name": "Equities",
      "entity_id": "019dfeb1-7c2d-7e0f-9a1b-2c3d4e5f6a7b",
      "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
      "rows": ["019f91c0-0a1b-7c2d-8e3f-4a5b6c7d8e9f"]
    }
  ],
  "next_cursor": null,
  "limit": 100,
  "as_of": "2026-10-17"
}

PATCH/v1/lenses/{id}

Update a lens

Partially updates a lens. rules replaces the rules as a whole — send the complete new set, not a delta. A new parent_id moves the lens within its client's tree (null moves it to the top); a move under the lens itself or one of its sub-lenses is 409 lens_cycle, with a next_action to move that sub-lens out first (PATCH /v1/lenses/{parent_id}, parent_id). A lens without rules only moves under a parent that holds all it holds (400 lens_needs_rules). null clears order, start_date and currency_code. The same 400 checks as on create apply, and unknown fields are a 400. The structure lens (locked) answers 409 locked with a next_action to rename its client instead (PATCH /v1/clients/{client_id}, name). Takes ?dry_run=true and Idempotency-Key.

status: active alone brings back a lens you archived, with the rules and accounts it had; status: archived is a 400 (archive with DELETE). Only a lens archived through v1 comes back; one the legacy platform deleted does not, and an archived lens takes no other change (404). A row that comes from the legacy platform answers 409 legacy_managed until your move to v1 is done.

Body parameters

namestring

New display name. Must not be blank.

parent_idstring | null

The lens to move it under, or null for the top.

show_as_portfolioboolean

Show it as a portfolio of its parent.

orderinteger | null

Its place among its siblings.

start_datestring | null

YYYY-MM-DD, or null for all history.

currency_codestring | null

ISO 4217, or null for the client's base currency.

rulesarray of object

Full replacement set of rules.

statusstring

active brings back an archived lens.

Request
PATCH/v1/lenses/{id}
curl -X PATCH https://api.aleta.ai/v1/lenses/019f91be-1d83-7f0a-bbfc-e64861142e87 \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "mode": "include", "kind": "custody_account", "ref": "019dfed3-16d5-7b8c-878c-aca5a2407b9e" },
      { "mode": "include", "kind": "custody_account", "ref": "019f91b0-a37b-7e76-afd4-f91dbc7f4b79" }
    ]
  }'
Response
{
  "id": "019f91be-1d83-7f0a-bbfc-e64861142e87",
  "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
  "parent_id": null,
  "name": "Seed lens",
  "show_as_portfolio": false,
  "order": null,
  "start_date": null,
  "locked": false,
  "currency_code": "DKK",
  "accounting_year_start": "01-01",
  "rules": [
    { "mode": "include", "kind": "custody_account", "ref": "019dfed3-16d5-7b8c-878c-aca5a2407b9e" },
    { "mode": "include", "kind": "custody_account", "ref": "019f91b0-a37b-7e76-afd4-f91dbc7f4b79" }
  ],
  "origin": "v1",
  "created_at": "2026-10-17T09:00:00.000000Z",
  "updated_at": "2026-10-17T09:05:00.000000Z",
  "status": "active",
  "archived_at": null,
  "archived_by": null,
  "archive_reason": null
}

DELETE/v1/lenses/{id}

Delete a lens

Archives a lens — the view only, never the underlying accounts or data. It leaves every list (unless include_archived=true) and every read with lens_id, and its definition is kept, so it can come back with PATCH /v1/lenses/{id} and status: active. Returns 204 on success, 404 if the id doesn't exist for this tenant or the lens is already archived. The structure lens is locked and answers 409 locked with a next_action to delete its client (DELETE /v1/clients/{client_id}), which it goes with. A lens with live sub-lenses, one a limit or benchmark points at, one a rule of another live lens names, or one a report definition is saved on answers 409 in_use with a next_action naming the first of them to archive, delete or change. A row that comes from the legacy platform answers 409 legacy_managed until your move to v1 is done. Takes ?dry_run=true and Idempotency-Key.

Request
DELETE/v1/lenses/{id}
curl -X DELETE https://api.aleta.ai/v1/lenses/019f95c5-afb9-7c30-aeec-ec385d540caf \
  -H "Authorization: Bearer {access_token}"
GET/v1/lenses/{id}/shares

Lens shares

Who the lens is shared with. A share gives no access: a user sees a shared lens only while the user sees its structure (an access grant on its client, an entity, or one of its cash or custody accounts), and loses it with the structure. Anyone who sees the lens reads its shares. For a lens from the legacy platform, until your move to v1 is done, these are the legacy platform's own: everyone who sees the lens's client (all_with_access: true), and the users it was shared with there; a user who is not an admin sees only themselves among those. The shares do not limit who sees a lens: everyone who sees its client does.

lens_idstring

The lens.

all_with_accessboolean

Shared with everyone who sees the lens's structure.

usersobject[]

{user_id, shared_at, shared_by_user_id, origin} per user it is shared with by name, in the order shared (users shared together by id).

Response
{
  "lens_id": "019f91be-1d83-7f0a-bbfc-e64861142e87",
  "all_with_access": false,
  "users": [
    {
      "user_id": "019e9a40-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "shared_at": "2026-10-17T09:00:00.000000Z",
      "shared_by_user_id": "019e9a40-0000-7000-8000-000000000001",
      "origin": "v1"
    }
  ]
}
PUT/v1/lenses/{id}/shares

Share a lens

Replaces the whole set, all_with_access included; there is no delete route, so {"all_with_access": false, "users": []} unshares the lens. The lens's owner (who made it) or an admin shares it; anyone else gets 403. Needs reporting:write and, for a user, the core module at read_write. Takes ?dry_run=true and Idempotency-Key (see Writes); a change is logged once, a set that changes nothing is not logged. Answers 200 with the shares.

all_with_accessboolean

Share with everyone who sees the lens's structure. Required.

usersobject[]

[{user_id}]: the users to share with by name. 400 for someone who is no user of the tenant or a user named twice; 422 no_structure_access for a user who sees none of the cash or custody accounts the lens holds (a folder: its sub-lenses').

A lens from the legacy platform answers 409 legacy_managed until your move to v1 is done. The structure lens is shared with everyone who sees its client and answers 409 locked.

cURL
curl -X PUT https://api.aleta.ai/v1/lenses/019f91be-1d83-7f0a-bbfc-e64861142e87/shares \
  -H "Authorization: Bearer {access_token}" \
  -H "Idempotency-Key: {uuid}" \
  -H "Content-Type: application/json" \
  -d '{"all_with_access": false, "users": [{"user_id": "019e9a40-1a2b-7c3d-8e4f-5a6b7c8d9e0f"}]}'