View as Markdown

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

idstring

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

client_idstring or null

The client the limit is on. For a limit from the legacy platform, the client of the lens it was set on.

target_kindstring or null

What the limit targets — lens, tag or asset, with target_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, or null until that lens exists in v1.

target_idstring or null

Identifier of the target, interpreted per target_kind.

namestring

Display name shown in the product.

kindstring

Rule kind — soft, hard, warning or custom.

limit_typestring

What the bounds measure: proportional (a weight of the parent, 0 to 1), absolute (an amount in currency_code) or folder (no bounds; it groups sub-limits and never breaches).

currency_codestring or null

The currency of an absolute limit's amounts. null when the legacy platform never said.

min_weightdecimal string or null

Lower bound: a weight for a proportional limit, an amount for an absolute one.

max_weightdecimal string or null

Upper bound.

target_weightdecimal string or null

The intended allocation, when the policy states one.

min_warning_weightdecimal string or null

Warning threshold above min_weight: a value below it reports warning.

max_warning_weightdecimal string or null

Warning threshold below max_weight: a value above it reports warning.

filterobject or null

What the limit counts, as a tree: {"type": "and" | "or", "filters": [...]} or {"type": "condition", "field", "operator", "values"}. field is lens, tag (a class or detailed class), asset, isin, country (ISO 3166) or currency (ISO 4217); operator is is or is_not. An and without filters counts the whole scope. Ids are v1 ids. null when v1 cannot say it exactly (see filter_unresolved).

filter_unresolvedboolean

true when 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. filter is then null.

parent_limit_idstring or null

The limit this one sits under.

ordernumber or null

The place among its siblings. null keeps creation order.

valid_fromISO 8601 date or null

First date the rule applies. null means since inception.

valid_toISO 8601 date or null

Last date the rule applies. null means open-ended.

originstring

legacy for a limit from the legacy platform (read-only until your move to v1 is done), v1 for one made here or in the Aleta web app.

statusstring

Lifecycle state — active or archived. An archived limit is no longer evaluated; its statuses and breaches stay.

archived_atRFC 3339 timestamp or null

When the limit was archived. null while it is active.

archived_bystring or null

The user who archived it: a person, or an API key's machine user.

archive_reasonstring or null

The reason sent with the archive.


GET/v1/limits

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_idstring

Only this client's limits, including legacy limits on its lenses. Not a UUID: 400; a client you cannot see gives an empty page.

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. A cursor this list did not mint is a 400.

Request
GET/v1/limits
curl -G https://api.aleta.ai/v1/limits \
  -H "Authorization: Bearer {access_token}" \
  -d client_id=019e0f2a-58c2-7b41-a3c5-24d1f0f1a3b9 \
  -d limit=1
Response
{
  "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
}

POST/v1/limits

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_weight or max_weight;
  • a proportional limit's bounds are weights from 0 to 1, and an absolute limit's min_weight is not negative;
  • min_weight ≤ target_weight ≤ max_weight;
  • min_warning_weight needs min_weight and lies above it, max_warning_weight needs max_weight and 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_idstring

The client the limit is on. Required. Unknown or hidden: 404.

namestring

Display name. Required.

kindstring

soft, hard, warning or custom. Optional, soft by default.

limit_typestring

proportional (default), absolute or folder.

currency_codestring

ISO 4217 code. Required for an absolute limit; DKK otherwise when left out.

filterobject

What the limit counts, in the shape a read returns. Optional: the whole scope when left out. A tag is a class or a detailed class you can see, otherwise 400; a lens or asset you cannot see is a 404.

target_kindstring

lens, tag or asset. Optional, pairs with target_id.

target_idstring

The target's identifier. Optional.

min_weightdecimal string

Lower bound. Optional — likewise max_weight, target_weight, min_warning_weight and max_warning_weight.

parent_limit_idstring

A limit on the same client to sit under, at most two levels deep. Optional; otherwise 400.

ordernumber

The place among its siblings. Optional.

valid_fromISO 8601 date

First date the rule applies. Optional — likewise valid_to.

Request
POST/v1/limits
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"] }
  }'
Response
{
  "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
}

GET/v1/limits/{id}

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.

Request
GET/v1/limits/{id}
curl https://api.aleta.ai/v1/limits/019f91bc-36b8-78e5-b813-f82a99de3337 \
  -H "Authorization: Bearer {access_token}"

PATCH/v1/limits/{id}

Update a limit

Partially updates a limit. Patchable fields:

  • client_id moves the limit, and you need write access to both clients;
  • name, kind, limit_type, currency_code, filter and order;
  • target_kind, target_id, the bounds, valid_from and valid_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.

Request
PATCH/v1/limits/{id}
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/v1/limits/{id}

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.

Request
DELETE/v1/limits/{id}
curl -X DELETE https://api.aleta.ai/v1/limits/019f91bf-fd8b-789d-b8f2-7732a2024977 \
  -H "Authorization: Bearer {access_token}"

POST/v1/limits/{id}/archive

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.

Request
POST/v1/limits/{id}/archive
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" }'

GET/v1/limits/{id}/status

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_idstring

The limit this status belongs to.

client_idstring or null

The limit's client.

as_ofISO 8601 date

The day asked for.

evaluated_onISO 8601 date or null

The day of the evaluation shown. null when there is none.

originstring

legacy or v1, as on the limit.

limit_typestring

proportional, absolute or folder.

currency_codestring or null

The currency of current_value.

statusstring

The worst of checks: ok, warning (inside the band but beyond a warning threshold), breach, or not_evaluated.

current_weightdecimal string or null

The weight of the worst check. null for an absolute limit.

current_valuedecimal string or null

The amount of the worst check, in currency_code.

acknowledgedboolean

true when the status is breach and its breach period was acknowledged.

checksarray

Every check of evaluated_on, worst first: id, status, current_weight, current_value and acknowledged.

Request
GET/v1/limits/{id}/status
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
Response
{
  "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
    }
  ]
}

GET/v1/limit-statuses

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.

Request
GET/v1/limit-statuses
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
Response
{
  "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"
}

GET/v1/limit-breaches

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

statusstring

open, acknowledged or resolved.

severitystring

breach or warning.

client_idstring

Only this client's limits.

limit_idstring

Only this limit. Not a UUID: 400.

include_archivedboolean

Also archived limits; their periods are resolved when as_of is on or after the day the limit was archived.

as_ofISO 8601 date

The day to read the periods on. Today when left out.

limitinteger

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

cursorstring

Opaque cursor from the previous page's next_cursor.

Breach fields

idstring

The period's id: the evaluation that opens it. It stays the same while the period lasts.

limit_idstring

The limit.

limit_namestring

The limit's display name, denormalized for convenience.

client_idstring or null

The limit's client.

originstring

legacy or v1, as on the limit.

severitystring

breach or warning.

statusstring

open, acknowledged (still going, and acknowledged), or resolved.

start_dateISO 8601 date

The first day of the period.

end_dateISO 8601 date or null

The last day of the period. null until it is resolved.

current_weightdecimal string or null

The measured value on the period's last day.

acknowledged_atRFC 3339 timestamp or null

When the period was first acknowledged here. null for an approval made on the legacy platform.

acknowledged_bystring or null

Who first acknowledged it here.

commentsarray

{comment, by, at}, oldest first; a comment from the legacy platform has by and at null.

Request
GET/v1/limit-breaches
curl -G https://api.aleta.ai/v1/limit-breaches \
  -H "Authorization: Bearer {access_token}" \
  -d status=open
Response
{
  "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"
}

POST/v1/limit-breaches/{id}/acknowledge

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

commentstring

Free-form acknowledgment note. Optional — so is the body itself. A blank comment is none.

Request
POST/v1/limit-breaches/{id}/acknowledge
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." }'
  • Performance — the numbers limits are evaluated against.
  • Tags — the classifications limit targets can reference.