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
idstringStable identifier for the lens. A UUID — treat it as opaque.
client_idstringThe client this view belongs to. Set at creation, immutable afterwards. A lens under another lens takes its parent's client.
parent_idstring | nullThe lens this one sits under,
nullat the top.namestringDisplay name shown in the product.
show_as_portfoliobooleanThe lens is shown as a portfolio of its parent. Only a lens with a
parent_idcan 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 | nullThe portfolio's place among its siblings, lowest first;
nullcomes last.start_datestring | nullYYYY-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.nullshows all history.lockedbooleanThe 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 | nullISO 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_startstringMM-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-01when none is set.rulesarray of objectWhat the lens holds:
mode(includeorexclude),kindandref(the id of what it names). See Rules. A lens synced from the legacy platform shows its accounts asincluderules.originstringlegacyfor a lens synced from the legacy platform (read-only until your move to v1 is done,409 legacy_managed),v1for one made here.created_atstringRFC 3339, UTC.
updated_atstringRFC 3339, UTC.
statusstringactive, orarchivedfor a lens you deleted (it comes back withPATCH status: active).archived_atstring | nullWhen the lens was archived, RFC 3339 UTC;
nullwhile it is active.archived_bystring | nullThe user who archived it;
nullwhile it is active.archive_reasonstring | nullAlways
nullfor 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.
kind | ref | holds |
|---|---|---|
client | client id | every account of the client |
entity | entity id | the entity's accounts, and its share of every entity it owns, down the ownership chain |
party | party id | the party's share of every entity it owns, directly or through other entities: 40 % owned shows 40 % |
ownership_path | ownership id (/v1/ownerships) | the owner's share along that one ownership, and on down through what the owned entity owns |
custody_account | custody account id | that account |
cash_account | cash account id | that account |
custodian | custodian id | the client's custody accounts at that custodian |
asset | asset id | the holdings of that asset |
tag | tag id (/v1/tags) | the holdings of the assets with that tag |
lens | lens id | what 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.
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_idstringOnly the lenses of this client.
parent_idstringOnly the lenses directly under this lens.
show_as_portfoliobooleantruelists the lenses shown as portfolios,false(the default) the others.include_archivedbooleantruelists archived lenses too. Defaults tofalse.limitintegerPage size. Defaults to 100, capped at 1 000.
cursorstringOpaque cursor from the previous page's
next_cursor. Omit for the first page.
curl -G https://api.aleta.ai/v1/lenses \
-H "Authorization: Bearer {access_token}" \
-d limit=2
{
"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
}
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_idstringThe client to create the view under. Required at the top; under a parent it is the parent's.
parent_idstringThe lens to create it under. Optional.
namestringDisplay name. Required.
show_as_portfoliobooleanShow it as a portfolio of its parent. Defaults to
false.orderintegerIts place among its siblings,
0or more. Optional.start_datestringYYYY-MM-DD. Optional.currency_codestringISO 4217. Optional.
rulesarray of objectWhat 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.
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" }
]
}'
{
"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
}
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.
curl https://api.aleta.ai/v1/lenses/019f91be-1d83-7f0a-bbfc-e64861142e87 \
-H "Authorization: Bearer {access_token}"
{
"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
}
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_ofstringYYYY-MM-DD. Defaults to today (UTC).limitintegerPage size. Defaults to 100, capped at 1 000.
cursorstringOpaque cursor from the previous page's
next_cursor.
Member
kindstringcustody_accountorcash_account.idstringThe account.
namestring | nullThe account's name.
entity_idstringThe entity that holds the account.
client_idstringThe account's client.
rowsstring[]The sub-lenses (and portfolios) of this lens the account shows under, in row order; empty for "everything else".
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
{
"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"
}
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
namestringNew display name. Must not be blank.
parent_idstring | nullThe lens to move it under, or
nullfor the top.show_as_portfoliobooleanShow it as a portfolio of its parent.
orderinteger | nullIts place among its siblings.
start_datestring | nullYYYY-MM-DD, ornullfor all history.currency_codestring | nullISO 4217, or
nullfor the client's base currency.rulesarray of objectFull replacement set of rules.
statusstringactivebrings back an archived lens.
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" }
]
}'
{
"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 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.
curl -X DELETE https://api.aleta.ai/v1/lenses/019f95c5-afb9-7c30-aeec-ec385d540caf \
-H "Authorization: Bearer {access_token}"
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_idstringThe lens.
all_with_accessbooleanShared 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).
{
"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"
}
]
}
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_accessbooleanShare with everyone who sees the lens's structure. Required.
usersobject[][{user_id}]: the users to share with by name.400for someone who is no user of the tenant or a user named twice;422 no_structure_accessfor 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 -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"}]}'
Related
- Cash accounts and custody accounts — the accounts a lens holds.
- Holdings — positions read against a lens: each holding weighed by the lens's rules, counted once.
- Performance — returns computed over a lens.