Limits
Limits encode investment-policy constraints — allocation bands, exposure caps, warning thresholds — and the platform evaluates them every day. Three resources work together: the limit (the rule), its status (the evaluation on a day), and breaches (the periods a rule was broken or in its warning band, with an acknowledgment on top). Reading requires the reporting:read scope and the limits module on read in one of your groups; every mutation requires reporting:write and the module on read/write (an admin always has the module; without it the route answers 403). Mutations also require write access to the limit's client (a manager grant on it, or the tenant owner/admin role); a firm-wide limit (no client) can only be changed by a tenant owner or admin. Otherwise the request fails with 403.
A limit from the legacy platform is on the client of the lens it was set on there. Until that lens exists in v1, and when the legacy platform hides the limit from customers, only a tenant owner or admin sees it. A limit the legacy platform deleted is not listed.
Data model
idstringStable identifier for the limit. A UUID — treat it as opaque.
client_idstring or nullThe client the limit is on. For a limit from the legacy platform, the client of the lens it was set on.
target_kindstring or nullWhat the limit targets —
lens,tagorasset, withtarget_id. A limit on a lens counts that lens's cash and custody accounts, and a proportional one weighs against the lens's value. A limit from the legacy platform targets the lens it was set on there, ornulluntil that lens exists in v1.target_idstring or nullIdentifier of the target, interpreted per
target_kind.namestringDisplay name shown in the product.
kindstringRule kind —
soft,hard,warningorcustom.limit_typestringWhat the bounds measure:
proportional(a weight of the parent, 0 to 1),absolute(an amount incurrency_code) orfolder(no bounds; it groups sub-limits and never breaches).currency_codestring or nullThe currency of an absolute limit's amounts.
nullwhen the legacy platform never said.min_weightdecimal string or nullLower bound: a weight for a proportional limit, an amount for an absolute one.
max_weightdecimal string or nullUpper bound.
target_weightdecimal string or nullThe intended allocation, when the policy states one.
min_warning_weightdecimal string or nullWarning threshold above
min_weight: a value below it reportswarning.max_warning_weightdecimal string or nullWarning threshold below
max_weight: a value above it reportswarning.filterobject or nullWhat the limit counts, as a tree:
{"type": "and" | "or", "filters": [...]}or{"type": "condition", "field", "operator", "values"}.fieldislens,tag(a class or detailed class),asset,isin,country(ISO 3166) orcurrency(ISO 4217);operatorisisoris_not. Anandwithout filters counts the whole scope. Ids are v1 ids.nullwhen v1 cannot say it exactly (seefilter_unresolved).filter_unresolvedbooleantruewhen the stored rule names something v1 has no word or id for: a single custody account, a GICS code, a rating range, an id with no v1 id, or a lens with no v1 record.filteris thennull.parent_limit_idstring or nullThe limit this one sits under.
ordernumber or nullThe place among its siblings.
nullkeeps creation order.valid_fromISO 8601 date or nullFirst date the rule applies.
nullmeans since inception.valid_toISO 8601 date or nullLast date the rule applies.
nullmeans open-ended.originstringlegacyfor a limit from the legacy platform (read-only until your move to v1 is done),v1for one made here or in the Aleta web app.statusstringLifecycle state —
activeorarchived. An archived limit is no longer evaluated; its statuses and breaches stay.archived_atRFC 3339 timestamp or nullWhen the limit was archived.
nullwhile it is active.archived_bystring or nullThe user who archived it: a person, or an API key's machine user.
archive_reasonstring or nullThe
reasonsent with the archive.
List limits
Returns every limit visible to the authenticated credential in the standard v1 list envelope, oldest first. An unknown parameter is a 400.
Query parameters
client_idstringOnly this client's limits, including legacy limits on its lenses. Not a UUID:
400; a client you cannot see gives an empty page.limitintegerPage size. Defaults to 100, capped at 1 000.
cursorstringOpaque cursor from the previous page's
next_cursor. Omit for the first page. A cursor this list did not mint is a400.
curl -G https://api.aleta.ai/v1/limits \
-H "Authorization: Bearer {access_token}" \
-d client_id=019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9 \
-d limit=1
{
"data": [
{
"id": "019f91bc-36b8-78e5-b813-f82a99de3337",
"client_id": "019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9",
"target_kind": null,
"target_id": null,
"name": "Danish equities",
"kind": "soft",
"limit_type": "proportional",
"currency_code": "DKK",
"min_weight": null,
"max_weight": "0.4",
"target_weight": null,
"min_warning_weight": null,
"max_warning_weight": "0.35",
"filter": {
"type": "condition",
"field": "country",
"operator": "is",
"values": ["DK"]
},
"filter_unresolved": false,
"parent_limit_id": null,
"order": null,
"valid_from": null,
"valid_to": null,
"origin": "v1",
"status": "active",
"archived_at": null,
"archived_by": null,
"archive_reason": null
}
],
"next_cursor": "MjAyNi0wNy0yNCAwMTozMTo0Ni4yNTc5NCswMHwwMTlmOTFiZi1mZDhiLTc4OWQtYjhmMi03NzMyYTIwMjQ5Nzc",
"limit": 1
}
Create limits
Creates one limit — or several at once. The response mirrors the request shape: send a single object and get the created object back; send an array and get an array back. Returns 201. All limits are written in one database transaction. An unknown field is a 400 that names it. Takes dry_run and Idempotency-Key.
The bounds follow the legacy rules, otherwise 400:
- a folder has none, and any other limit sets
min_weightormax_weight; - a proportional limit's bounds are weights from 0 to 1, and an absolute limit's
min_weightis not negative; min_weight≤target_weight≤max_weight;min_warning_weightneedsmin_weightand lies above it,max_warning_weightneedsmax_weightand lies below it;- every value is below 10 000 000 000 in size.
Also 400: an empty array, a blank name, a kind other than soft, hard, warning or custom, a currency_code that is not three letters A-Z, an absolute limit without currency_code, target_kind without target_id (or the reverse), and valid_to without valid_from or before it.
Body parameters
client_idstringThe client the limit is on. Required. Unknown or hidden:
404.namestringDisplay name. Required.
kindstringsoft,hard,warningorcustom. Optional,softby default.limit_typestringproportional(default),absoluteorfolder.currency_codestringISO 4217 code. Required for an absolute limit;
DKKotherwise when left out.filterobjectWhat the limit counts, in the shape a read returns. Optional: the whole scope when left out. A
tagis a class or a detailed class you can see, otherwise400; a lens or asset you cannot see is a404.target_kindstringlens,tagorasset. Optional, pairs withtarget_id.target_idstringThe target's identifier. Optional.
min_weightdecimal stringLower bound. Optional — likewise
max_weight,target_weight,min_warning_weightandmax_warning_weight.parent_limit_idstringA limit on the same client to sit under, at most two levels deep. Optional; otherwise
400.ordernumberThe place among its siblings. Optional.
valid_fromISO 8601 dateFirst date the rule applies. Optional — likewise
valid_to.
curl -X POST https://api.aleta.ai/v1/limits \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"client_id": "019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9",
"name": "Danish equities",
"max_weight": "0.4",
"max_warning_weight": "0.35",
"filter": { "type": "condition", "field": "country", "operator": "is", "values": ["DK"] }
}'
{
"id": "019f91bc-36b8-78e5-b813-f82a99de3337",
"client_id": "019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9",
"target_kind": null,
"target_id": null,
"name": "Danish equities",
"kind": "soft",
"limit_type": "proportional",
"currency_code": "DKK",
"min_weight": null,
"max_weight": "0.4",
"target_weight": null,
"min_warning_weight": null,
"max_warning_weight": "0.35",
"filter": {
"type": "condition",
"field": "country",
"operator": "is",
"values": ["DK"]
},
"filter_unresolved": false,
"parent_limit_id": null,
"order": null,
"valid_from": null,
"valid_to": null,
"origin": "v1",
"status": "active",
"archived_at": null,
"archived_by": null,
"archive_reason": null
}
Retrieve a limit
Fetch a single limit by id. Returns the bare object, or a 404 problem when the id doesn't exist for this tenant, is not a UUID, or names a limit you cannot see.
curl https://api.aleta.ai/v1/limits/019f91bc-36b8-78e5-b813-f82a99de3337 \
-H "Authorization: Bearer {access_token}"
Update a limit
Partially updates a limit. Patchable fields:
client_idmoves the limit, and you need write access to both clients;name,kind,limit_type,currency_code,filterandorder;target_kind,target_id, the bounds,valid_fromandvalid_to.
target_kind, target_id, the bounds, order and the dates can be cleared by passing null. Omitted fields are unchanged; an unknown field is a 400. When a PATCH changes limit_type or a bound, the bounds are checked as on create, on the limit as it will be. The change is made to the limit as stored when the write starts, so two updates at once never lose one another.
status: "active" brings back an archived limit; status: "archived" is a 400 (archive with the archive route). An optional reason goes to the change log. A limit that comes from the legacy platform answers 409 legacy_managed until your move to v1 is done. Takes dry_run and Idempotency-Key.
curl -X PATCH https://api.aleta.ai/v1/limits/019f91bc-36b8-78e5-b813-f82a99de3337 \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "max_weight": "0.45", "reason": "Board decision 2026-10" }'
Delete a limit
Removes a limit that has never been evaluated. Returns 204 on success, 404 if the id doesn't exist for this tenant.
A limit that has been checked (it, or a limit under it, has a status) is not deleted: the answer is 409 in_use, and next_action points to POST /v1/limits/{id}/archive. A limit that comes from the legacy platform answers 409 legacy_managed until your move to v1 is done. Takes dry_run and Idempotency-Key.
curl -X DELETE https://api.aleta.ai/v1/limits/019f91bf-fd8b-789d-b8f2-7732a2024977 \
-H "Authorization: Bearer {access_token}"
Archive a limit
Retires a limit without deleting it: the rule stops being evaluated but stays on record, with its statuses and breaches. Returns 200 with the limit, its status now archived and archived_at, archived_by and archive_reason set. The body is optional: {"reason": "..."}. Archiving an archived limit changes nothing and answers it as it is. PATCH with status: "active" brings it back.
A limit that comes from the legacy platform answers 409 legacy_managed until your move to v1 is done. Takes dry_run and Idempotency-Key.
curl -X POST https://api.aleta.ai/v1/limits/019f91bc-36b8-78e5-b813-f82a99de3337/archive \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "reason": "Replaced by the 2027 policy" }'
Retrieve a limit's status
Returns one limit's evaluation on as_of (a date; today when left out): its checks on the latest evaluated day on or before as_of, one per lens that day was evaluated for. Nothing is computed here; the figures are the stored evaluation, and a day from the legacy platform keeps the legacy result. A limit reports not_evaluated when it has no evaluation yet or is not in force on as_of (outside its validity, or archived by then). An unknown parameter is a 400.
Status fields
limit_idstringThe limit this status belongs to.
client_idstring or nullThe limit's client.
as_ofISO 8601 dateThe day asked for.
evaluated_onISO 8601 date or nullThe day of the evaluation shown.
nullwhen there is none.originstringlegacyorv1, as on the limit.limit_typestringproportional,absoluteorfolder.currency_codestring or nullThe currency of
current_value.statusstringThe worst of
checks:ok,warning(inside the band but beyond a warning threshold),breach, ornot_evaluated.current_weightdecimal string or nullThe weight of the worst check.
nullfor an absolute limit.current_valuedecimal string or nullThe amount of the worst check, in
currency_code.acknowledgedbooleantruewhen the status isbreachand its breach period was acknowledged.checksarrayEvery check of
evaluated_on, worst first:id,status,current_weight,current_valueandacknowledged.
curl -G https://api.aleta.ai/v1/limits/019f91bc-36b8-78e5-b813-f82a99de3337/status \
-H "Authorization: Bearer {access_token}" \
-d as_of=2026-10-05
{
"limit_id": "019f91bc-36b8-78e5-b813-f82a99de3337",
"client_id": "019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9",
"as_of": "2026-10-05",
"evaluated_on": "2026-10-05",
"origin": "v1",
"limit_type": "proportional",
"currency_code": "DKK",
"status": "warning",
"current_weight": "0.37",
"current_value": "3700000",
"acknowledged": false,
"checks": [
{
"id": "019f9a01-7c33-7e42-9a1d-3f1c2b0d4e55",
"status": "warning",
"current_weight": "0.37",
"current_value": "3700000",
"acknowledged": false
}
]
}
List limit statuses
Returns every visible limit's status on as_of (today when left out; the envelope shows it), one entry per limit, oldest limit first. Same shape as the single-limit status. Takes client_id (only that client's limits), limit and cursor. Archived limits are left out unless you pass include_archived=true. An unknown parameter is a 400.
curl -G https://api.aleta.ai/v1/limit-statuses \
-H "Authorization: Bearer {access_token}" \
-d client_id=019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9 \
-d limit=1
{
"data": [
{
"limit_id": "019f91bc-36b8-78e5-b813-f82a99de3337",
"client_id": "019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9",
"as_of": "2026-10-18",
"evaluated_on": null,
"origin": "v1",
"limit_type": "proportional",
"currency_code": "DKK",
"status": "not_evaluated",
"current_weight": null,
"current_value": null,
"acknowledged": false,
"checks": []
}
],
"next_cursor": null,
"limit": 1,
"as_of": "2026-10-18"
}
List limit breaches
Returns breach periods as of as_of (today when left out; the envelope shows it), oldest start first. A period is the days in a row a limit was in breach, or in its warning band; it ends when a later day is in another state or the limit is no longer in force. A day from the legacy platform keeps the legacy result, and a period the legacy platform approved is acknowledged. A limit the legacy platform deleted shows no breach. By default only open and acknowledged periods are listed. An unknown parameter or value is a 400.
Query parameters
statusstringopen,acknowledgedorresolved.severitystringbreachorwarning.client_idstringOnly this client's limits.
limit_idstringOnly this limit. Not a UUID:
400.include_archivedbooleanAlso archived limits; their periods are resolved when
as_ofis on or after the day the limit was archived.as_ofISO 8601 dateThe day to read the periods on. Today when left out.
limitintegerPage size. Defaults to 100, capped at 1 000.
cursorstringOpaque cursor from the previous page's
next_cursor.
Breach fields
idstringThe period's id: the evaluation that opens it. It stays the same while the period lasts.
limit_idstringThe limit.
limit_namestringThe limit's display name, denormalized for convenience.
client_idstring or nullThe limit's client.
originstringlegacyorv1, as on the limit.severitystringbreachorwarning.statusstringopen,acknowledged(still going, and acknowledged), orresolved.start_dateISO 8601 dateThe first day of the period.
end_dateISO 8601 date or nullThe last day of the period.
nulluntil it is resolved.current_weightdecimal string or nullThe measured value on the period's last day.
acknowledged_atRFC 3339 timestamp or nullWhen the period was first acknowledged here.
nullfor an approval made on the legacy platform.acknowledged_bystring or nullWho first acknowledged it here.
commentsarray{comment, by, at}, oldest first; a comment from the legacy platform hasbyandatnull.
curl -G https://api.aleta.ai/v1/limit-breaches \
-H "Authorization: Bearer {access_token}" \
-d status=open
{
"data": [
{
"id": "019f9a01-7c33-7e42-9a1d-3f1c2b0d4e55",
"limit_id": "019f91bc-36b8-78e5-b813-f82a99de3337",
"limit_name": "Danish equities",
"client_id": "019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9",
"origin": "v1",
"severity": "breach",
"status": "open",
"start_date": "2026-10-05",
"end_date": null,
"current_weight": "0.43",
"acknowledged_at": null,
"acknowledged_by": null,
"comments": []
}
],
"next_cursor": null,
"limit": 100,
"as_of": "2026-10-18"
}
Acknowledge a breach
Records that a person has seen and accepted a breach period — the compliance sign-off. It covers the whole period, open or resolved. The first acknowledgment records when and by whom, and that never changes. A later one with a comment adds the comment, and one without news changes nothing. There is no way to take an acknowledgment back. Returns 200 with the breach.
Errors come in this order: 404 when the id is unknown to you or opens no period; 403 without write access to the limit's client; 409 legacy_managed for a limit from the legacy platform until your move to v1 is done. Takes dry_run and Idempotency-Key.
Body parameters
commentstringFree-form acknowledgment note. Optional — so is the body itself. A blank comment is none.
curl -X POST https://api.aleta.ai/v1/limit-breaches/019f9a01-7c33-7e42-9a1d-3f1c2b0d4e55/acknowledge \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{ "comment": "Rebalancing scheduled for month-end." }'
Related
- Performance — the numbers limits are evaluated against.
- Tags — the classifications limit targets can reference.