> ## Documentation Index
> Fetch the complete documentation index at: https://docs.allocait.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Get a portfolio snapshot (unpriced)

> This is the discovery call: unpriced. Returns a single denormalized
blob with the portfolio, the holdings in shares, the active
scenario's rules, the recorded levels, and monitor configuration.
`current_price`, `position_value` and `position_pct` on every
non-cash position, `total_value`, `exposure`, `compliance`, and every
monitor measurement all come back null (and `rebalance`, when you ask for it). `symbols_to_price`
lists every symbol you would need to price to see the rest. A held
option is the exception: it is priced at its `mark`, the user's own
number, so it keeps its `current_price` and `position_value` here,
and `expired_on` and `expired_options` show what has expired
(ADR-147).

Allocait never fills a symbol from its own market data on this
surface (ADR-036). The flow is two calls: GET this to see what's
held, then POST here with `prices` for the symbols it listed (or
call `/compliance/check` / `/rebalance/preview` directly, which
require full coverage).

The `rebalance` block (sized amounts) is left out unless you add
`include_rebalance=true`. Ask for it when the person wants to know
what it would take to get back inside a band; a check answer comes
from `compliance` (ADR-113).

The `monitors` block reports where each recorded level stands at the
prices you pass. It is a reading, not a break. A break is confirmed
only by the once-daily close check, which this endpoint does not
report. `GET /monitors` returns each monitor's full configuration.

If `scenario_id` is omitted, the portfolio's `active_scenario_id` is used.




## OpenAPI

````yaml /openapi.yaml get /portfolio/snapshot
openapi: 3.1.0
info:
  title: Allocait Agent API
  version: 1.0.0
  contact:
    name: Allocait support
    email: support@allocait.app
    url: https://docs.allocait.app/support
  license:
    name: Proprietary
    url: https://allocait.app/terms
  description: >
    Allocait checks your holdings against rules you wrote. It does not give
    investment advice or recommend trades.


    Use this API to read your rules and holdings, check them at prices you

    supply, and keep positions and cash current.


    New here? The [API quickstart](https://docs.allocait.app/api/quickstart)

    covers a key, a first read, a first check and error handling.


    **Currency.** Allocait supports US dollars only. Money values are in USD.


    **Authentication.** Send your API key in the `x-api-key` header.

    Create keys in the Allocait app under Settings > API keys. Every

    request is scoped to the key owner's own portfolios, and no request can

    reach another user's data.


    **Versioning.** The major version is in the path, and every path in

    this file is relative to a base that ends in `/v1`. A request to a path

    with no version, or to a version that does not exist, gets `404` with

    `error_code: NOT_FOUND` (a request with no valid key gets `401` first).
    There is no version header to send, and

    `info.version` above is the revision of this file, not the path

    version. The intent for `/v1`: changes are additive (a new route, a

    new response field, a new optional parameter, a new value in a

    response enum), so ignore fields you do not know and treat an

    `error_code` you do not know by its HTTP status. A breaking change

    ships as `/v2` beside `/v1`, and the docs announce the old version, with

    `Deprecation` and `Sunset` headers, before it is removed.


    **Prices.** Every price that leaves this API is one you supplied.

    Allocait's own stored quote is never returned, and nothing computed from

    it is either: not a value, a compliance verdict, a trade, or a monitor

    reading. The flow is two calls: `GET /portfolio/snapshot` to see what a

    portfolio holds and which symbols still need a price

    (`symbols_to_price`), then call again with a `prices` map for those

    symbols: `POST /portfolio/snapshot`, `/compliance/check`,

    `/rebalance/preview` or `/trades/check`. `/trades/check` also needs a

    price for every symbol in `trades`. One kind of number is the person's

    own and not a quote: the `mark` of an option, the premium per share they

    recorded. A held option with a mark needs no entry in `prices`, and

    an option past its expiry counts 0 whatever you pass (ADR-147).


    **Permissions.** A key carries `read`, `write`, or both. Every GET and

    every compute-only POST (`/compliance/check`, `/rebalance/preview`,

    `/portfolio/snapshot`, `/validate`, `/trades/check`, `/positions/size`,

    `/setup/preview`) needs `read`;

    every route marked as a write needs `write`. A key without the right

    one gets `403` with `This API key does not have read permission` or

    `Write permission required` (the second carries `error_code:

    READ_ONLY_KEY`, the code MCP gives it). When the account's agent

    access is read-only, no key can hold `write`, so that refusal says

    `This account's agent access is read-only.` instead, with the same

    code and status (ADR-232). This rule applies to

    every route; each operation's own `403` description lists only the

    causes specific to it. One route reads the body to decide: `POST

    /scenarios/{id}/rules` needs `read` when `dry_run` is `true` and

    `write` otherwise.


    **Plans.** An account on Core or Advanced with a paying or trialing

    subscription can use this API. Core reads: every GET and every

    compute-only POST works, and a key that carries `write` is capped to

    `read` at request time, so a write route answers `403` with `Write

    permission required`. Advanced reads and changes, and is the only plan

    with webhooks. A key issued on Advanced reads only after a downgrade

    and works again after an upgrade. An account with no active plan gets one
    `403` text on

    every route: `This Allocait account has no active plan, so agent access is
    off. The user manages their account in the Allocait app.`


    **Rate limits.** Every route shares one 60-requests-per-minute budget

    per account, shared with the MCP server and across all of the

    account's API keys. Every response that reaches a route carries

    `X-RateLimit-Limit` (always `60`) and `X-RateLimit-Remaining` (how many

    are left in the current window). A request past the cap gets `429` with

    those headers and `Retry-After: 60` instead of a route response. The

    same account also has a ceiling of 10,000 requests per UTC day; past it,

    requests get `429` with `Retry-After` set to the seconds until 00:00

    UTC, and no `X-RateLimit-*` headers (they would say requests remain).

    `401` and the no-plan `403` carry none of these headers. A per-route cap

    (`DAILY_LIMIT_EXCEEDED`, 20 support requests or feedback reports in 24

    hours) is also a `429`, with the `X-RateLimit-*` headers and no

    `Retry-After`, because waiting a minute does not clear it. See the `429`

    response on each operation.


    **List responses.** Every GET list route returns `{data, has_more}`,

    never a bare array. `data` is the page of rows and `has_more` is `true`

    when another page follows. The GET list routes that take `limit`

    (default 100, max 500) and `cursor` (a row offset, default 0) query

    parameters (`/portfolios`, `/positions`, `/watchlists`, `/scenarios`,

    `/asset-classes`, `/charts`, `/playbooks`, `/position-rules`,

    `/support-requests`, `/agent-feedback`, `/actions`, `/inputs`,

    `/inputs/{id}/entries`) page on them: call again with

    `cursor` set to the previous `cursor` (or 0) plus `limit` while

    `has_more` is `true`. `/monitors`, `/watchlists/{id}/items`,

    `/inputs/{id}/options` and `/webhooks` take no

    `limit`/`cursor` and always return their complete result, so `has_more`

    on those is always `false`. A list scoped to an id (a portfolio, an

    input, a watchlist, a scenario) answers `404` when that id is not

    yours, never an empty `data` that reads like a list with nothing in it.

    `/rules/restore-points` is a distinct shape

    (`{success, restore_points}`, already holding at most the caller's most

    recent 20) and does not use this envelope at all.


    **Method not allowed.** A real path called with a verb it does not

    support gets `405`, not the `404` an unknown path gets, with an `Allow`

    header listing the methods that path does support.


    **Request format.** Every POST/PATCH/DELETE body must be JSON. A request

    sent with a `Content-Type` that is present and not `application/json`

    gets `400` with `Content-Type must be application/json` before any

    other validation runs. A request with no `Content-Type` at all is still

    parsed as JSON, unchanged. A body that is present but does not parse as

    JSON gets its own `400` (`Request body is not valid JSON`), distinct

    from the "field is required" a genuinely empty body produces.


    **Field types and ids.** A field of the wrong type is a `400` that names

    it (`initial_cash must be a number`), never a default: a number sent as

    text does not become 0. On a create, `null` means "no value", the same as

    leaving the field out. An id in the query or body that is not a UUID is a

    `400` too (`portfolio_id must be a UUID`). In the path, an id of the right

    length with the wrong characters is a `400` (`The id in the path must be a

    UUID`), and any other shape matches no route and is a `404`. A mistake in

    the request should get a `400`, not a `500`.


    **Bulk write limits.** A bulk write caps these array fields at 500 items per

    request: `positions` (`POST /portfolios/{id}/positions/apply`), `rules`

    (`POST /scenarios/{id}/position-rules/apply`), `entries`

    (`POST /inputs/entries`), `entry_ids` (`DELETE /inputs/entries`),

    `item_ids` and `sort_orders` (`PATCH /watchlists/{id}/items`), and

    `action_ids` (`DELETE /actions`).

    Sending more gets `400` with `<field> must have at most 500 items`. The

    `rules_text` field on `POST /scenarios/{id}/rules` has its own two caps:

    the text itself at 102,400 bytes (100 KiB), checked before the file is

    parsed at all (`400` with `rules_text must be at most 102400 bytes`), and

    the file's `classes` at 500, checked right after parsing and before the

    RPC call (`400` with `classes must have at most 500 items`).


    **Not investment advice.** Every read here is a mechanical result of

    rules and data you supplied: a compliance check against your own

    declared bands, a rebalance preview toward your own declared targets.

    Nothing in this API opines on any security's merit, and it never says

    whether to buy, sell, or hold anything. Every write is confined to your

    own account and is always the absolute state you're asserting, never a

    delta and never an order. Allocait output states facts about

    the user's own rules. It is not advice or an approval. See the Terms of

    Service ("Not Investment Advice") for the full statement this API

    implements.


    **No stored answer about the person.** No field on this API records or

    returns how a person invests or which first-run lane they chose; the

    answer on the first screen is counted and not stored.


    **Two ranges on every rule.** A position rule carries an allocation band

    (`min_position_pct` / `max_position_pct`) on the asset class total, which

    is what `/compliance/check`, `/validate` and `/rebalance/preview`

    evaluate, and an optional position size range (`size_min_pct` /

    `size_max_pct`) for one holding, which `/compliance/check` and

    `/validate` report in `size_violations` and `/positions/size` sizes

    against. A rule may also carry a short size range

    (`short_size_min_pct` / `short_size_max_pct`) for one short holding,

    judged by its size without the minus sign. All are percentages of

    total portfolio value. A rule with a size range may have no allocation

    band: both band fields are then `null` and the class total is not

    judged, while each holding is. A band of 0 to 0 means none of the class.

    The snapshot

    also names the band `band_min_pct` / `band_max_pct` and the target

    `target_pct`, next to the older names, which stay.


    **Asset class names.** A body field that takes an asset class by name

    (`asset_class`) resolves it in one order: your own class with that

    name, then the system class, then the rules text alias (Stocks or ETFs →

    Equities, Bonds → Fixed Income, Forex → Currencies). Names compare

    case-insensitively after trimming, and an id is accepted too.


    **Duplicate names.** Every create route (`POST /portfolios`,

    `/scenarios`, `/asset-classes`, `/watchlists`, `/inputs`, `/playbooks`,

    `/monitors`, `/charts`) refuses a name that's already taken with `409`

    and `error_code: ALREADY_EXISTS`, carrying `existing_id`, so a retried

    create never makes a duplicate. Names compare case-insensitively after

    trimming: per portfolio for playbooks, monitors and charts, and per

    account for everything else. Names are at most 100 characters. Each

    account also has a ceiling on how many actions, custom asset classes,

    Inputs, recorded values, options, playbooks and charts it holds; a write

    past one is `TIER_LIMIT_EXCEEDED`, with `limit_type`, `current_count` and

    `max_allowed`.


    **Error codes and statuses.** Every refusal a route produces carries an

    `error_code` from one closed catalog shared with the MCP server:

    `INVALID_INPUT`, `NOT_FOUND`, `ALREADY_EXISTS`, `IN_USE`,

    `CONFIRMATION_REQUIRED`, `PRICES_INCOMPLETE`, `PRICES_INVALID`,

    `WOULD_CLOSE_POSITIONS`, `INSUFFICIENT_CASH`, `RULES_INVALID`,

    `TARGET_OUT_OF_BAND`, `TIER_LIMIT_EXCEEDED`, `FEATURE_NOT_AVAILABLE`,

    `IDEMPOTENCY_KEY_CONFLICT`, `DAILY_LIMIT_EXCEEDED`, `READ_ONLY_KEY`,

    `OAUTH_WRITE_NOT_GRANTED`, `TOOL_REMOVED` and `INTERNAL_ERROR`.

    `OAUTH_WRITE_NOT_GRANTED`, `WOULD_CLOSE_POSITIONS` and `TOOL_REMOVED`

    occur on MCP only. The status follows the code, on every route:

    `400` for `INVALID_INPUT`, `PRICES_INCOMPLETE`, `PRICES_INVALID`,

    `RULES_INVALID` and `TARGET_OUT_OF_BAND`; `403` for

    `TIER_LIMIT_EXCEEDED`, `FEATURE_NOT_AVAILABLE` and `READ_ONLY_KEY`;

    `404` for `NOT_FOUND`, which is also the answer for an id that is not

    yours, so an id cannot be probed; `409` for `ALREADY_EXISTS`, `IN_USE`,

    `CONFIRMATION_REQUIRED`, `INSUFFICIENT_CASH` and

    `IDEMPOTENCY_KEY_CONFLICT`; `429` for `DAILY_LIMIT_EXCEEDED`; `500` for

    `INTERNAL_ERROR`, which also carries a `request_id` to quote when you

    report it. `405` and `413` keep their HTTP statuses with

    `INVALID_INPUT`. Four answers are outside the catalog and carry no

    `error_code`: `401`, the no-plan `403`, the `403` for a key without

    `read`, and the `429` for the request budget (branch on the status for

    those). See the `Error` schema for the extra fields each code carries.
servers:
  - url: https://allocait.app/api/v1
    description: Hosted Allocait API
security:
  - ApiKeyAuth: []
tags:
  - name: Portfolios
    description: >-
      List, create, update and delete portfolios, and read one portfolio's full
      snapshot. The snapshot is the discovery call and lists the symbols you
      need to price.
  - name: Holdings and cash
    description: >-
      Read positions, set absolute share counts, size a position, and record
      deposits and withdrawals. Allocait records what you hold. It never places
      a trade.
  - name: Checks
    description: >-
      Compare a portfolio, or a change to it, with your rules at prices you
      supply. A check states how far a rule is outside its range. It never says
      what to trade.
  - name: Rules
    description: >-
      Scenarios, asset classes, allocation bands, position size ranges and the
      portable rules text, with restore points to undo an import.
  - name: Watchlists
    description: Named, ordered lists of symbols to follow.
  - name: Inputs
    description: >-
      Values you track by hand, such as a support level or a macro reading, with
      their dropdown options and recorded entries.
  - name: Playbooks
    description: >-
      Saved research views. A playbook resolves its symbols and shows the input
      values for any date you ask about, with each value's recorded date.
  - name: Monitors
    description: >-
      Condition monitors hold the levels or ranges you record for a symbol, read
      against the price you pass.
  - name: Charts
    description: Saved chart configurations for a portfolio.
  - name: Actions
    description: The record of position and cash changes, with an editable comment on each.
  - name: Webhooks
    description: Signed event notifications for compliance changes. Advanced plan.
  - name: Support and feedback
    description: >-
      File support requests and feedback about this API, and list your own. Each
      is capped at 20 per rolling 24 hours.
  - name: Preferences
    description: Which portfolio, watchlist, chart, playbook and monitor the app opens to.
  - name: Setup file
    description: >-
      Preview, apply and undo an ALLOCAIT.md setup file, and export the
      definitions of your watchlists, Inputs, playbooks, monitors and saved
      charts as one versioned JSON file. Definitions only, never a value, price
      or holding.
externalDocs:
  description: API quickstart
  url: https://docs.allocait.app/api/quickstart
paths:
  /portfolio/snapshot:
    get:
      tags:
        - Portfolios
      summary: Get a portfolio snapshot (unpriced)
      description: >
        This is the discovery call: unpriced. Returns a single denormalized

        blob with the portfolio, the holdings in shares, the active

        scenario's rules, the recorded levels, and monitor configuration.

        `current_price`, `position_value` and `position_pct` on every

        non-cash position, `total_value`, `exposure`, `compliance`, and every

        monitor measurement all come back null (and `rebalance`, when you ask
        for it). `symbols_to_price`

        lists every symbol you would need to price to see the rest. A held

        option is the exception: it is priced at its `mark`, the user's own

        number, so it keeps its `current_price` and `position_value` here,

        and `expired_on` and `expired_options` show what has expired

        (ADR-147).


        Allocait never fills a symbol from its own market data on this

        surface (ADR-036). The flow is two calls: GET this to see what's

        held, then POST here with `prices` for the symbols it listed (or

        call `/compliance/check` / `/rebalance/preview` directly, which

        require full coverage).


        The `rebalance` block (sized amounts) is left out unless you add

        `include_rebalance=true`. Ask for it when the person wants to know

        what it would take to get back inside a band; a check answer comes

        from `compliance` (ADR-113).


        The `monitors` block reports where each recorded level stands at the

        prices you pass. It is a reading, not a break. A break is confirmed

        only by the once-daily close check, which this endpoint does not

        report. `GET /monitors` returns each monitor's full configuration.


        If `scenario_id` is omitted, the portfolio's `active_scenario_id` is
        used.
      operationId: getPortfolioSnapshot
      parameters:
        - name: portfolio_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: scenario_id
          in: query
          required: false
          description: Defaults to the portfolio's active scenario
          schema:
            type: string
            format: uuid
        - name: include_rebalance
          in: query
          required: false
          description: >-
            Add the `rebalance` block. Default `false`. Any other value than
            `true` or `false` is a `400`.
          schema:
            type: boolean
      responses:
        '200':
          description: Portfolio snapshot
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortfolioSnapshot'
              example:
                scenario_id: 3f2e940e-c6d0-4710-ac50-7d981b4ef2dc
                portfolio:
                  portfolio_id: 919bbfb9-c2e7-4c4c-9d6d-d20f83330245
                  name: Main Portfolio
                  description: Your primary investment portfolio
                  base_currency: USD
                  active_scenario_id: 3f2e940e-c6d0-4710-ac50-7d981b4ef2dc
                  active_scenario_name: My rules
                  allow_shorts: false
                total_value: null
                price_source: unpriced
                symbols_to_price:
                  - ALFA
                  - BRAV
                  - GOLF
                positions:
                  - symbol: ALFA
                    shares: 250
                    is_cash: false
                    asset_class_id: d238ab31-0a5f-4a33-99e3-b828f2d4c316
                    asset_class: Equities
                    moves: 1
                    mark: null
                    mark_on: null
                    expired_on: null
                    current_price: null
                    position_value: null
                    position_pct: null
                  - symbol: BRAV
                    shares: 200
                    is_cash: false
                    asset_class_id: 768714f9-2f74-4712-ae2a-a8cff76267b0
                    asset_class: Fixed Income
                    moves: 1
                    mark: null
                    mark_on: null
                    expired_on: null
                    current_price: null
                    position_value: null
                    position_pct: null
                  - symbol: GOLF
                    shares: 300
                    is_cash: false
                    asset_class_id: a5abe5cf-9a5c-42e4-ad3c-f50ba87186fd
                    asset_class: Commodities
                    moves: 1
                    mark: null
                    mark_on: null
                    expired_on: null
                    current_price: null
                    position_value: null
                    position_pct: null
                  - symbol: $CASH
                    shares: 17095
                    is_cash: true
                    asset_class_id: 38dc3624-7d8b-41d4-83c9-6162c0b99323
                    asset_class: Cash
                    moves: 1
                    mark: null
                    mark_on: null
                    expired_on: null
                    current_price: 1
                    position_value: 17095
                    position_pct: null
                expired_options: 0
                exposure: null
                rules:
                  - asset_class_id: 38dc3624-7d8b-41d4-83c9-6162c0b99323
                    asset_class: Cash
                    band_min_pct: 6
                    band_max_pct: 19
                    target_allocation_pct: null
                    min_position_pct: 6
                    max_position_pct: 19
                    size_min_pct: null
                    size_max_pct: null
                    short_size_min_pct: null
                    short_size_max_pct: null
                    target_pct: null
                  - asset_class_id: a5abe5cf-9a5c-42e4-ad3c-f50ba87186fd
                    asset_class: Commodities
                    band_min_pct: 4
                    band_max_pct: 13
                    target_allocation_pct: 9
                    min_position_pct: 4
                    max_position_pct: 13
                    size_min_pct: null
                    size_max_pct: null
                    short_size_min_pct: null
                    short_size_max_pct: null
                    target_pct: 9
                  - asset_class_id: d238ab31-0a5f-4a33-99e3-b828f2d4c316
                    asset_class: Equities
                    band_min_pct: 38
                    band_max_pct: 56
                    target_allocation_pct: 47
                    min_position_pct: 38
                    max_position_pct: 56
                    size_min_pct: null
                    size_max_pct: null
                    short_size_min_pct: null
                    short_size_max_pct: null
                    target_pct: 47
                  - asset_class_id: 768714f9-2f74-4712-ae2a-a8cff76267b0
                    asset_class: Fixed Income
                    band_min_pct: 14
                    band_max_pct: 29
                    target_allocation_pct: 21
                    min_position_pct: 14
                    max_position_pct: 29
                    size_min_pct: null
                    size_max_pct: null
                    short_size_min_pct: null
                    short_size_max_pct: null
                    target_pct: 21
                compliance: null
                monitors: []
                generated_at: '2026-10-14T08:58:04.315714+00:00'
        '400':
          description: >-
            Missing or malformed `portfolio_id` or `scenario_id`, or an
            `include_rebalance` that is not `true` or `false`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Key lacks `read` permission, or API access needs an active plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Portfolio or scenario not found, or not yours (`NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
components:
  headers:
    RateLimitLimit:
      description: >-
        The per-minute request budget, always `60`. Sent on every response that
        reaches a route and on the per-minute `429`.
      schema:
        type: integer
        example: 60
    RateLimitRemaining:
      description: >-
        Requests left in the current one-minute window, after this one. The
        daily count is not reported until it is reached.
      schema:
        type: integer
        example: 41
    RetryAfter:
      description: >-
        Seconds to wait before retrying. Sent only on the request budget's own
        `429`s, never on a per-route cap (`DAILY_LIMIT_EXCEEDED`) or any other
        status.
      schema:
        type: integer
        example: 60
  schemas:
    PortfolioSnapshot:
      type: object
      description: >
        Single denormalized blob: portfolio + positions + rules +

        compliance violations + condition monitors, and rebalance amounts

        when the request sets `include_rebalance=true`.

        The agent's primary "look at the world" response. See ADR-007

        and ADR-026. A check answer comes from `compliance`; sized amounts

        are there only when asked for (ADR-113). `prices_complete` /
        `unpriced_symbols` (the fields

        `ComplianceResponse` / `RebalanceResponse` both omit for the same

        reason) are not carried on this surface either: both are computed

        from Allocait's own stored quote, not from your coverage of it, so

        neither can be scrubbed the way `current_price`, `total_value` and

        `exposure` are; use `symbols_to_price` instead (ADR-036).
      properties:
        portfolio:
          $ref: '#/components/schemas/SnapshotPortfolio'
        scenario_id:
          type:
            - string
            - 'null'
          format: uuid
        total_value:
          type:
            - number
            - 'null'
          description: >-
            Null when any non-cash position is unpriced; a value spanning the
            whole portfolio can't stand on one stored quote (ADR-036). Present
            only once every non-cash position is covered by your `prices` map or
            owns its price (an option with a mark, or an expired one, ADR-147).
            Signed, since shorts subtract, so it can be zero or negative. At
            zero or less every `position_pct` is null and `compliance` reads
            `unjudged` (ADR-153).
        exposure:
          description: |
            How much of the portfolio gains if prices rise, as percents of its
            value: long, short, net and gross, by asset class and per holding.
            A reading, not a rule; there is no range, cap or status to compare
            it with. Always present. Null unless your `prices` map holds a
            number for every non-cash holding, and while any holding is
            unpriced, even where Allocait holds a quote of its own that could
            fill the gap: every percent is a share of the whole book, so one
            stored quote would reach all of them (ADR-036, ADR-142). A holding
            that owns its price, an option with a mark or an expired one,
            counts as covered. Options count at their premium (ADR-147).
            Computed from your prices and the user's marks, and never stored.
          oneOf:
            - $ref: '#/components/schemas/ExposureReading'
            - type: 'null'
        price_source:
          type: string
          enum:
            - overrides
            - unpriced
          description: |
            `overrides` once every symbol this response needed came from
            your `prices` map (`symbols_to_price` is then empty);
            `unpriced` while at least one still does not. Allocait's own
            stored quote (`market_data`) never appears on this surface
            (ADR-036).
        symbols_to_price:
          type: array
          items:
            type: string
          description: >-
            Every symbol, sorted, that you would need to add to `prices` to
            clear every null from this response. Empty once the payload is
            entirely your own numbers.
        positions:
          type: array
          items:
            $ref: '#/components/schemas/SnapshotPosition'
        expired_options:
          type: integer
          description: >-
            How many held options have passed their expiry (`expired_on` set,
            shares not 0). The user's own record, so it needs no price and is
            present whatever else is null. Allocait changes nothing on them and
            sends no alert; the person decides what each became (ADR-147).
        rules:
          type: array
          description: >-
            The active scenario's position rules. Same fields as `GET
            /position-rules` minus the ids and timestamps.
          items:
            type: object
            properties:
              asset_class_id:
                type: string
                format: uuid
              asset_class:
                type: string
              min_position_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Lower bound of the allocation band on the asset class total,
                  as % of portfolio value. Null, with `max_position_pct`, when
                  the rule has no band; the class total is not judged.
              max_position_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Upper bound of the allocation band on the asset class total,
                  as % of portfolio value. Null when the rule has no band.
              target_allocation_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Optional aim point inside the allocation band that `rebalance`
                  moves toward.
              band_min_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  The same value as `min_position_pct`, under the name the MCP
                  tools use.
              band_max_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  The same value as `max_position_pct`, under the name the MCP
                  tools use.
              target_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  The same value as `target_allocation_pct`, under the name the
                  MCP tools use.
              size_min_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Lower bound of the position size range for one holding in this
                  class, as % of portfolio value. Null when no size range is
                  set.
              size_max_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Upper bound of the position size range for one holding in this
                  class, as % of portfolio value. Null when no size range is
                  set.
              short_size_min_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Lower bound of the short size range for one short in this
                  class, as % of portfolio value. Null when no short size range
                  is set.
              short_size_max_pct:
                type:
                  - number
                  - 'null'
                description: >-
                  Upper bound of the short size range for one short in this
                  class, as % of portfolio value. Null when no short size range
                  is set.
        compliance:
          description: |
            Null when any non-cash position is unpriced. An `{error}`
            object when the portfolio has no scenario to evaluate against.
            Otherwise the shape POST /compliance/check returns, computed
            from this request's `prices`, including `status`, `issue_count`
            and `unchecked`.
          oneOf:
            - $ref: '#/components/schemas/SnapshotCompliance'
            - $ref: '#/components/schemas/Error'
            - type: 'null'
        rebalance:
          description: |
            Present only when the request sets `include_rebalance=true`
            (ADR-113); left out otherwise. Same nullability as
            `compliance`: null when any non-cash position is unpriced, an
            `{error}` object when no scenario is set, otherwise the same
            shape POST /rebalance/preview returns, computed from this
            request's `prices`.
          oneOf:
            - $ref: '#/components/schemas/RebalanceResponse'
            - $ref: '#/components/schemas/Error'
            - type: 'null'
        monitors:
          type: array
          description: |
            Each condition monitor measured against the levels and ranges
            the user recorded, at the price you passed for its symbol (null,
            unmeasured, if you did not). One entry per (monitor, resolved
            symbol); `[]` when the portfolio has no monitors. See ADR-020
            and ADR-026.
          items:
            $ref: '#/components/schemas/SnapshotMonitor'
        playbooks_needing_input:
          type: array
          description: |
            The portfolio's playbooks with at least one blank cell today
            (US Eastern, or the playbook's pinned date), as the playbook's
            `needs_input` counts them: blank cells of Inputs that are not
            optional. `[]` when none. The user's own recorded data, never
            priced (ADR-204).
          items:
            type: object
            properties:
              playbook_id:
                type: string
                format: uuid
              name:
                type: string
              date:
                type: string
                format: date
              blank_count:
                type: integer
        generated_at:
          type: string
          format: date-time
    Error:
      type: object
      description: >
        Uniform error envelope returned by every non-2xx response. `error` is

        a human-readable message, safe to show the user. A refusal also

        carries `error_code`, a stable string from the catalog in the info

        block's Error codes section, to branch on; the HTTP status follows

        the code (ADR-134). Only `401`, the no-plan `403`, the `403` for a

        key without `read` and the `429` for the request budget have none.

        A `500` carries `request_id`. Extra fields by code:

        TIER_LIMIT_EXCEEDED carries `limit_type`, `current_count` and

        `max_allowed`, so a caller can branch on how many it has versus the

        cap without a second call. Its `error` text names no plan. It covers

        the portfolio and scenario limits and the account caps: 20 webhook
        endpoints, 50

        watchlists, 200 symbols on one watchlist and 50 custom symbols on one

        monitor. Don't retry it. RULES_INVALID (`POST

        /scenarios/{id}/rules`) carries `issues`. ALREADY_EXISTS carries

        `existing_id`, plus `duplicate_ids` where older duplicates exist.

        CONFIRMATION_REQUIRED carries `would_remove`; from `POST /setup/apply`
        it

        carries a fresh preview instead (`digest`, `file`, `items` and the rest
        of

        `SetupPreview`). INVALID_INPUT from the setup routes carries `issues`,
        and

        IN_USE from `POST /setup/receipts/{id}/undo` carries `later_receipt_id`.

        NOT_FOUND on a name, and

        INVALID_INPUT on a CHOICE value, carry `valid_values`. INVALID_INPUT on
        a

        monitor whose level input is not a number type carries `input_id`.

        IN_USE on an input whose type would change while a monitor uses it

        carries `monitors`. IN_USE on `PATCH /portfolios/{id}` that turns

        Allow shorts off while a short is held carries `short_count` and

        `symbols`. INVALID_INPUT on the holdings writes (`POST

        /positions/{symbol}`, `POST /portfolios/{id}/positions/apply`) that

        refuses a negative `shares`, a `moves` other than 1 on an Options

        holding, or a `mark` on a holding outside the Options class, carries

        `symbols`, the holdings it refused. (`symbols` also appears on
        `WOULD_CLOSE_POSITIONS`, MCP

        only, where it lists the holdings a full list left out.)
      required:
        - error
      example:
        error: Portfolio not found or access denied
        error_code: NOT_FOUND
      properties:
        error:
          type: string
          description: Human-readable error message.
        error_code:
          type: string
          enum:
            - INVALID_INPUT
            - NOT_FOUND
            - ALREADY_EXISTS
            - IN_USE
            - CONFIRMATION_REQUIRED
            - PRICES_INCOMPLETE
            - PRICES_INVALID
            - WOULD_CLOSE_POSITIONS
            - INSUFFICIENT_CASH
            - RULES_INVALID
            - TARGET_OUT_OF_BAND
            - TIER_LIMIT_EXCEEDED
            - FEATURE_NOT_AVAILABLE
            - IDEMPOTENCY_KEY_CONFLICT
            - DAILY_LIMIT_EXCEEDED
            - READ_ONLY_KEY
            - OAUTH_WRITE_NOT_GRANTED
            - TOOL_REMOVED
            - INTERNAL_ERROR
          description: >-
            Stable machine-readable code from the closed catalog. Present on
            every refusal a route produces; absent on `401`, the no-plan `403`,
            the `403` for a key without `read`, and the `429` for the request
            budget.
        request_id:
          type: string
          format: uuid
          description: >-
            Present on a `500` (`INTERNAL_ERROR`). The same id is in Allocait's
            logs next to the real cause; quote it when you report the failure.
        issues:
          type: array
          items:
            anyOf:
              - $ref: '#/components/schemas/RulesIssue'
              - $ref: '#/components/schemas/SetupIssue'
          description: >-
            Every error and warning found while reading the rules text, when
            error_code is RULES_INVALID. From the setup routes (`POST
            /setup/preview`, `POST /setup/apply`) it is `SetupIssue` rows, on
            INVALID_INPUT.
        later_receipt_id:
          type: string
          format: uuid
          description: >-
            The newer import of the same setup file that blocks this undo.
            Present on IN_USE from `POST /setup/receipts/{id}/undo`; undo that
            receipt first.
        limit_type:
          type: string
          description: >-
            Which cap was hit, e.g. "portfolios", "scenarios",
            "webhook_endpoints", "watchlists", "asset_classes", "input_entries"
            or "input_entries_per_input". Present only when error_code is
            TIER_LIMIT_EXCEEDED.
        current_count:
          type: number
          description: >-
            How many of limit_type the caller has right now. Present only when
            error_code is TIER_LIMIT_EXCEEDED.
        max_allowed:
          type: number
          description: >-
            The cap for limit_type on this account. Present only when error_code
            is TIER_LIMIT_EXCEEDED.
        existing_id:
          type: string
          format: uuid
          description: >-
            The record that already has this name. Present only when error_code
            is ALREADY_EXISTS; change it with that id instead of creating
            another.
        duplicate_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Other records with the same name, created before duplicates were
            refused. Present only on ALREADY_EXISTS, and only when such records
            exist.
        would_remove:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          description: >-
            What the call would drop, keyed by list field, for example the
            `items` that a watchlist save would remove. Present only when
            error_code is CONFIRMATION_REQUIRED; resend with those members in
            `remove`.
        valid_values:
          type: array
          items:
            type: string
          description: >-
            The names or options the field accepts. Present on NOT_FOUND for a
            name, and on INVALID_INPUT for a CHOICE value.
        input_id:
          type: string
          format: uuid
          description: >-
            The level input that is not a NUMBER, DECIMAL or PERCENTAGE input.
            Present on INVALID_INPUT from a monitor create or update (`POST
            /monitors`, `PATCH /monitors/{id}`, MCP `save_monitor`); point the
            monitor at a number-type input instead.
        short_count:
          type: integer
          description: >-
            How many shorts the portfolio holds. Present on IN_USE from `PATCH
            /portfolios/{id}` (MCP `save_portfolio`) that turns Allow shorts off
            while a short is held; close them first.
        symbols:
          type: array
          items:
            type: string
          description: >-
            Holdings the call names. On IN_USE from `PATCH /portfolios/{id}`,
            the shorts still held (up to 20, by symbol). On INVALID_INPUT from
            the holdings writes, the holdings whose negative `shares` was
            refused (Allow shorts is off, or the holding is in Options), or
            whose `moves` other than 1 was refused (the holding is in Options).
            On WOULD_CLOSE_POSITIONS (MCP only), the holdings a full list left
            out.
        monitors:
          type: array
          description: >-
            The monitors that use this input as a level. Present on IN_USE from
            an input update (`PATCH /inputs/{id}`, MCP `save_input`) that would
            change `data_type` to TEXT or CHOICE; move those monitors to another
            input first.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              portfolio_id:
                type: string
                format: uuid
    SnapshotPortfolio:
      type: object
      description: |
        The portfolio header as carried inside `PortfolioSnapshot.portfolio`.
        `PortfolioSummary` without `position_count`; the RPC builds this
        object itself and does not count positions here.
      properties:
        portfolio_id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        base_currency:
          type: string
          enum:
            - USD
          description: Always USD. Allocait supports US dollars only.
        active_scenario_id:
          type:
            - string
            - 'null'
          format: uuid
        active_scenario_name:
          type:
            - string
            - 'null'
        allow_shorts:
          type: boolean
          description: >-
            Whether holdings may carry a negative share count (a short). Off by
            default (ADR-153).
    ExposureReading:
      type: object
      description: |
        How much of the portfolio gains if prices rise, as percents of
        portfolio value (cash plus the signed priced holdings, options
        included). A reading, not a verdict: nothing in it is judged against
        a range. For each non-cash holding outside the Options class that has
        a price, exposure is `shares x price x moves`, with `shares` signed:
        a short counts negative, a short in an inverse fund counts positive,
        and a fund held long at -1x counts short. An option counts at its
        premium: `shares x price`, where the price is its `mark` (or the price
        you passed for this call), positive for a call and negative for a put,
        with no Moves, and 0 once it has expired (ADR-147). Premium
        understates how far an option moves, so the reading is labelled, not
        adjusted. Cash counts in neither direction. A holding with no price is
        left out, and so is a priced option whose symbol does not parse as an
        option symbol (it has no direction): `options_not_counted` counts those
        and `options_not_counted_symbols` names them. A portfolio worth zero or
        less has no percent: the four figures are null and both lists are
        empty. Priced at the prices you passed and never stored (ADR-002). Size
        and exposure are different lenses: a 1% holding in an inverse 3x fund
        is 1% by size and -3% by exposure.
      required:
        - total_value
        - long_pct
        - short_pct
        - net_pct
        - gross_pct
        - options_counted
        - options_not_counted
        - options_not_counted_symbols
        - classes
        - holdings
      properties:
        total_value:
          type: number
          description: >-
            Portfolio value, the denominator of every percent. Equal to the
            snapshot's `total_value` once every holding is priced.
          example: 100000
        long_pct:
          type:
            - number
            - 'null'
          description: >-
            Sum of the positive exposures as a percent of `total_value`, two
            decimals.
          example: 86
        short_pct:
          type:
            - number
            - 'null'
          description: >-
            Absolute sum of the negative exposures as a percent of
            `total_value`. A positive number.
          example: 9
        net_pct:
          type:
            - number
            - 'null'
          description: >-
            `long_pct` minus `short_pct`. Negative when the book gains if prices
            fall.
          example: 77
        gross_pct:
          type:
            - number
            - 'null'
          description: '`long_pct` plus `short_pct`.'
          example: 95
        options_counted:
          type: integer
          description: >-
            Options inside the four figures. Unexpired, priced, with a symbol
            that parses. Above 0 is the app's cue to label the reading "options
            at premium".
          example: 1
        options_not_counted:
          type: integer
          description: >-
            Priced options in the Options class left out of the four figures
            because the symbol is not an option symbol, so it has no direction.
            They still count in `total_value`.
          example: 0
        options_not_counted_symbols:
          type: array
          items:
            type: string
          description: The symbols behind `options_not_counted`, sorted.
          example: []
        classes:
          type: array
          description: >-
            One entry per asset class with exposure, biggest gross first. A
            holding with no class is an entry with a null `asset_class_id` and
            `asset_class`.
          items:
            type: object
            required:
              - asset_class_id
              - asset_class
              - long_pct
              - short_pct
              - net_pct
            properties:
              asset_class_id:
                type:
                  - string
                  - 'null'
                format: uuid
              asset_class:
                type:
                  - string
                  - 'null'
              long_pct:
                type: number
              short_pct:
                type: number
                description: Positive.
              net_pct:
                type: number
        holdings:
          type: array
          description: >-
            Every holding with a nonzero exposure, largest first. `moves` is the
            setting the exposure used.
          items:
            type: object
            required:
              - symbol
              - asset_class_id
              - moves
              - exposure_value
              - exposure_pct
            properties:
              symbol:
                type: string
              asset_class_id:
                type:
                  - string
                  - 'null'
                format: uuid
              moves:
                type: integer
                enum:
                  - 1
                  - 2
                  - 3
                  - -1
                  - -2
                  - -3
                description: Always 1 for an option; Moves does not apply to one.
              exposure_value:
                type: number
                description: >-
                  `shares x price x moves`, signed. For an option, `shares x
                  price`, positive for a call and negative for a put.
              exposure_pct:
                type: number
                description: '`exposure_value` as a signed percent of `total_value`.'
              basis:
                type: string
                enum:
                  - premium
                description: >-
                  Present on an option only. It counts at its premium, which
                  understates how far it moves.
              option_type:
                type: string
                enum:
                  - call
                  - put
                description: Present on an option only, read from its symbol.
      example:
        total_value: 100000
        long_pct: 86
        short_pct: 10
        net_pct: 76
        gross_pct: 96
        options_counted: 1
        options_not_counted: 0
        options_not_counted_symbols: []
        classes:
          - asset_class_id: d238ab31-0a5f-4a33-99e3-b828f2d4c316
            asset_class: Equities
            long_pct: 86
            short_pct: 9
            net_pct: 77
          - asset_class_id: 5b0e3f6a-8c1d-4b7e-9a42-6d1f0c8e2b97
            asset_class: Options
            long_pct: 0
            short_pct: 1
            net_pct: -1
        holdings:
          - symbol: ALFA
            asset_class_id: d238ab31-0a5f-4a33-99e3-b828f2d4c316
            moves: 1
            exposure_value: 86000
            exposure_pct: 86
          - symbol: BRAV
            asset_class_id: d238ab31-0a5f-4a33-99e3-b828f2d4c316
            moves: 1
            exposure_value: -6000
            exposure_pct: -6
          - symbol: CHAR
            asset_class_id: d238ab31-0a5f-4a33-99e3-b828f2d4c316
            moves: -3
            exposure_value: -3000
            exposure_pct: -3
          - symbol: ECHO281215P00050000
            asset_class_id: 5b0e3f6a-8c1d-4b7e-9a42-6d1f0c8e2b97
            moves: 1
            exposure_value: -1000
            exposure_pct: -1
            basis: premium
            option_type: put
    SnapshotPosition:
      type: object
      description: |
        One holding as carried inside `PortfolioSnapshot.positions`. Narrower
        than `Position` from `GET /positions` (no `id`, `portfolio_id` or
        `position_sizing`) and priced only as far as the
        request's `prices` map reaches.
      properties:
        symbol:
          type: string
        shares:
          type: number
          description: >-
            Signed. Negative is a short (ADR-153); its `position_value` and
            `position_pct` are negative too. For a holding filed under the
            Options system asset class, recorded as contracts × 100 at the
            per-share premium (ADR-056).
        is_cash:
          type: boolean
        asset_class_id:
          type:
            - string
            - 'null'
          format: uuid
        asset_class:
          type:
            - string
            - 'null'
        moves:
          type: integer
          enum:
            - 1
            - 2
            - 3
            - -1
            - -2
            - -3
          description: >-
            Times its index a fund moves; negative is inverse. 1 unless the user
            set it (ADR-142). Present on every row, cash included.
        mark:
          type:
            - number
            - 'null'
          description: >-
            The per-share premium the user recorded for an option; null off the
            Options class and for an option with no mark (ADR-147). The user's
            own number.
        mark_on:
          type:
            - string
            - 'null'
          format: date
          description: >-
            The day (US Eastern) the mark was last set, changed, or confirmed by
            a trade price. Null when there is no mark.
        expired_on:
          type:
            - string
            - 'null'
          format: date
          description: >-
            The option's expiry date once it has passed, else null. An expired
            option's `current_price` is 0 whatever you pass.
        current_price:
          type:
            - number
            - 'null'
          description: >-
            Cash is always 1. For any other position, the price you passed for
            its symbol, or null if you did not (including on a GET, which passes
            none), never Allocait's own stored quote (ADR-036). An option is the
            exception, priced at its `mark` (your price for the call replaces
            it) or at 0 once it has expired, because both are the user's own
            numbers. An option with no mark and no price from you is null.
        position_value:
          type:
            - number
            - 'null'
          description: >-
            shares × current_price. Null under the same conditions as
            current_price (never null for cash).
        position_pct:
          type:
            - number
            - 'null'
          description: >-
            Share of total portfolio value. Null whenever any non-cash position
            is unpriced, since one stored quote in the sum would contaminate
            every position's share. Also null when the portfolio is worth zero
            or less, where a share is undefined (ADR-153).
    SnapshotCompliance:
      description: |
        The snapshot's compliance block: everything `ComplianceResponse`
        carries, the one-word `status` included. Each class and holding
        violation also carries `band_min_pct` / `band_max_pct` next to
        `min_pct` / `max_pct`, and each per-holding violation carries
        `holding_pct` and `class_pct`.
      allOf:
        - $ref: '#/components/schemas/ComplianceResponse'
    RebalanceResponse:
      type: object
      properties:
        portfolio_id:
          type: string
          format: uuid
        scenario_id:
          type: string
          format: uuid
        total_value:
          type: number
          description: >-
            Signed. A portfolio worth zero or less has no percentages, so
            `trades` is empty and the class percentages are null; the total
            itself is still reported (ADR-153).
        price_source:
          type: string
          enum:
            - overrides
          description: >-
            Always `overrides` on this surface. `prices` is required and checked
            to cover every non-cash position before this is computed, except a
            held option that owns its price (ADR-036, ADR-147).
        trade_count:
          type: integer
        trades:
          type: array
          description: >-
            Pro rata amounts, ordered largest-dollar-value first. A holding
            priced at 0 gets no row, since an amount cannot be priced at 0
            (ADR-080). A short (negative `shares`) or an option (a holding in
            the Options class) gets no row; its class's move is spread over the
            class's long holdings outside Options only (ADR-153, ADR-145).
          items:
            $ref: '#/components/schemas/Trade'
        resulting_classes:
          type: array
          description: >-
            Each asset class's share of the portfolio after these amounts, Cash
            included. Proceeds from removed shares stay in Cash. Every
            `resulting_pct` is null when the portfolio is worth zero or less
            (ADR-153).
          items:
            type: object
            properties:
              asset_class_id:
                type: string
                format: uuid
              asset_class:
                type: string
              resulting_pct:
                type:
                  - number
                  - 'null'
        basis:
          type: string
          description: >-
            Pro rata amounts that would move each class toward its aim: the
            user's target, or the midpoint of their band. Arithmetic on the
            user's own rules; not a recommendation.
        unresolved:
          type: array
          description: >-
            Classes still outside their band after these amounts, such as a
            class below its band with no holding to add to, each with the
            reason. The amounts don't resize holdings, so size_violations stand.
          items:
            type: object
            properties:
              asset_class_id:
                type: string
                format: uuid
              asset_class:
                type: string
              resulting_pct:
                type: number
              band_min_pct:
                type: number
              band_max_pct:
                type: number
              reason:
                type: string
                enum:
                  - no_priced_holdings
                  - zero_price
                  - cash
                  - outside_band_after_trades
                description: |
                  `no_priced_holdings`: the class has nothing the preview may
                  trade, whatever its prices (a short and an option are not
                  traded).
                  `zero_price`: every holding in the class is priced at 0, so
                  none can be traded (ADR-080). `cash`: Cash moves only as
                  other amounts settle. `outside_band_after_trades`: rounding
                  to share units leaves the class just outside its band.
        not_rebalanced:
          type: array
          description: |
            Every short and every Options class holding the rebalance leaves
            as it is, once each, ordered by symbol (ADR-145). Always present,
            empty when there are none. A holding with zero shares is not
            listed. An entry carries no price, count or value.
          items:
            type: object
            required:
              - symbol
              - asset_class
              - reason
            properties:
              symbol:
                type: string
              asset_class:
                type:
                  - string
                  - 'null'
              reason:
                type: string
                enum:
                  - short
                  - option
                description: >-
                  `short`: a negative share count. `option`: a holding in the
                  Options class.
    SnapshotMonitor:
      type: object
      description: >
        One condition monitor's intraday range position for one resolved

        symbol, as carried inside `PortfolioSnapshot.monitors`.


        This is the VIEW half of ADR-020's monitor model: a measurement, not

        an assertion of a break. Reading it is free, changes nothing, and is

        never pushed; only a settled daily close is ever treated as a

        confirmed break, by a separate once-daily mechanism this response

        does not report. Every bound and threshold here is a value the user

        recorded as an Input (ADR-024); Allocait computes none of them.


        Narrower than `ConditionMonitorData` from `GET /monitors`, and

        renamed throughout. Field mapping between the two, for a caller

        holding both responses. The left column names columns of the

        `GET /monitors` row; the `input_1_id` / `input1_name` /

        `input_2_id` / `input2_name` / `conditions` / `condition_values`

        columns are present in that response but not itemised in the

        `ConditionMonitorData` schema below.


        | `ConditionMonitorData` (`GET /monitors`) | `SnapshotMonitor` (here) |

        | --- | --- |

        | `input_1_id` | `recorded_low_input_id` |

        | `input1_name` | `recorded_low_input_name` |

        | `input1_value` (string) | `recorded_low` (number, or null) |

        | `input1_date` (string) | `recorded_low_date` (string, or null) |

        | `input_2_id` | `recorded_high_input_id` |

        | `input2_name` | `recorded_high_input_name` |

        | `input2_value` (string) | `recorded_high` (number, or null) |

        | `input2_date` (string) | `recorded_high_date` (string, or null) |

        | `conditions`, `condition_values` | not carried; stay on `GET
        /monitors` |


        `GET /monitors` no longer carries `price`, `prev_close`,

        `change_pct`, `position_pct`, `price_status_type`,

        `price_status_label` or `price_status_color` at all: those were

        computed from Allocait's own stored quote and are dropped from that

        response under ADR-036. The priced reading (`price`,

        `range_position_pct` **or** `distance_from_level_pct`, and

        `price_state`) lives only here, at the price you pass.


        Beyond the renames, the values themselves diverge in four documented

        ways, all deliberate:


        1. An unpriced symbol reports `price: null`, not `0`.

        2. A bound the user never recorded, or recorded as text that is not
           a number, yields `price_state: null` rather than falling through
           to `in_range` or being measured as a level.
        3. `bound_kind` follows the monitor's configuration, so a range with
           one bound still missing stays `range` here while `GET /monitors`
           treats it as level-shaped.
        4. An inverted recorded pair (`recorded_high` below `recorded_low`)
           yields a null measurement and a null `price_state` instead of a
           positional word that points the wrong way.
      properties:
        monitor_id:
          type: string
          format: uuid
        monitor_name:
          type: string
          description: The user's own name for this monitor
        symbol:
          type: string
          example: ALFA
        is_label:
          type: boolean
          description: >-
            True when the symbol is a label (ADR-239). A label is never in
            `symbols_to_price`; it reads `unpriced` unless a price for it is
            passed.
        comparison_mode:
          type:
            - string
            - 'null'
          enum:
            - ABOVE
            - BELOW
            - NEAR
            - RANGE
            - null
          description: |
            The user's own configured mode, passed through unchanged.
            Nothing in this object is derived from it: `price_state`
            describes where price sits, never whether the mode is satisfied.
            Null when the monitor was created without one: the column is
            nullable and `condition_monitor_create` writes an explicit NULL
            rather than falling back to the column default. Allocait does
            not invent a mode the user never chose.
        bound_kind:
          type: string
          enum:
            - range
            - level
          description: |
            `range` when the monitor is configured with a second bound Input
            (`recorded_high_input_id` is set), `level` when it has only one.
            Derived from the monitor's configuration, not from
            `comparison_mode` and not from what has been recorded so far.

            A `range` monitor whose bounds are not both on record is still
            `range`: `recorded_low` and/or `recorded_high` come back null
            and `range_position_pct` and `price_state` are null with them.
            Do not read `bound_kind: range` as a promise that two numbers
            are present. Check the bounds themselves. The alternative,
            deriving the shape from the recorded values, would re-label a
            half-recorded range as a `level` and measure price against the
            user's floor as though it were a standalone level.
        recorded_low:
          type:
            - number
            - 'null'
          description: |
            The latest value the user recorded against the monitor's first
            bound Input (`recorded_low_input_id`). In `level` shape this is
            the single recorded level. Null when nothing numeric is on
            record for it against this symbol, including when the recorded
            entry is free text that is not a number, since
            `input_entries.value` is untyped.

            "Low" names the slot and makes no claim about the value: nothing
            constrains the first Input's value to sit below the second's,
            and Allocait never reorders the pair (ADR-024; it may not
            derive a bound the user did not record). When a caller sees
            `recorded_high` below `recorded_low`, the pair is inverted and
            the measurement fields are null for that reason.
        recorded_high:
          type:
            - number
            - 'null'
          description: |
            The latest value recorded against the monitor's second bound
            Input (`recorded_high_input_id`). Always null in `level` shape,
            where there is no second Input; also null in `range` shape when
            the user has not yet recorded a value for it, or recorded one
            that is not a number.
        recorded_low_date:
          type:
            - string
            - 'null'
          format: date
          description: |
            The date (`YYYY-MM-DD`) of the entry `recorded_low` came from: the
            newest entry the user recorded against the first bound Input for
            this symbol, whatever its date, so a future-dated entry shows its
            own. Null when no entry exists. Set even when `recorded_low` is
            null because that newest entry is text that is not a number. A
            fact about the entry, not a judgement of its age: compare it with
            your own clock.
        recorded_high_date:
          type:
            - string
            - 'null'
          format: date
          description: >-
            The date of the entry `recorded_high` came from. Always null in
            `level` shape; otherwise as `recorded_low_date`.
        recorded_low_input_id:
          type: string
          format: uuid
          description: The Input definition `recorded_low` came from
        recorded_low_input_name:
          type:
            - string
            - 'null'
        recorded_high_input_id:
          type:
            - string
            - 'null'
          format: uuid
        recorded_high_input_name:
          type:
            - string
            - 'null'
        threshold_pct:
          type:
            - number
            - 'null'
          description: |
            The user's own proximity setting, governing `near_low` /
            `near_high` / `near_level` display only. Unrelated to close
            confirmation. Without it none of the three appears: a range reads
            `at_low`, `at_high`, `in_range`, `below_range` or `above_range`, a
            level reads `at_level`, `above_level` or `below_level`.
        price:
          type:
            - number
            - 'null'
          description: |
            The price you passed for this symbol. Null otherwise, never 0,
            which would read as a break rather than a data gap. Allocait's
            own stored quote never appears here (ADR-036).
        price_source:
          type: string
          enum:
            - overrides
            - unpriced
          description: |
            Per-symbol, unlike the top-level `price_source`. `overrides`
            when you priced this symbol, `unpriced` when you did not.
            Allocait's own stored quote (`market_data`) never appears on
            this surface (ADR-036).
        range_position_pct:
          type:
            - number
            - 'null'
          description: |
            `range` shape only. Where price sits between the user's two
            recorded bounds: 0 at `recorded_low`, 100 at `recorded_high`,
            negative below and above 100 above. Null in `level` shape, when
            the symbol is unpriced, when either bound is missing or not
            numeric, when the two bounds are equal, and when the pair is
            inverted (`recorded_high` below `recorded_low`).

            Together with `distance_from_level_pct` these are the two halves
            of what `GET /monitors` used to carry as `position_pct` before
            ADR-036 dropped it from that response; the two are never both
            non-null here. Not to be confused with
            `SnapshotPosition.position_pct` in the same response, which is a
            holding's share of portfolio value.
        distance_from_level_pct:
          type:
            - number
            - 'null'
          description: |
            `level` shape only. Signed percent of price away from the single
            recorded level. Null in `range` shape, when unpriced, when no
            level is recorded or the recorded value is not numeric, and when
            the recorded level is 0.

            The level half of what `GET /monitors` used to carry as
            `position_pct`; see `range_position_pct` for the range half.
        price_state:
          type:
            - string
            - 'null'
          enum:
            - below_range
            - above_range
            - at_low
            - at_high
            - near_low
            - near_high
            - in_range
            - at_level
            - near_level
            - above_level
            - below_level
            - null
          description: |
            Where price sits, stated flatly. Same vocabulary as the
            `price_status_type` `GET /monitors` carried before ADR-036
            dropped it from that response. Null whenever the corresponding
            measurement is null: no price, a bound that is missing or not
            numeric, equal bounds, or an inverted pair. Never a verdict: no
            value here means a break, a signal, or a confirmation.
    RulesIssue:
      type: object
      description: |
        One problem or note found while reading rules text. A file
        carrying any `error`-level issue is never applied; `POST
        /scenarios/{id}/rules` 400s with `error_code: RULES_INVALID` and
        the full `issues` list instead. A file with only `warning`-level
        issues still applies; those ride along on the 200 response.
      required:
        - level
        - message
      properties:
        level:
          type: string
          enum:
            - error
            - warning
        message:
          type: string
        class:
          type: string
          description: >-
            The asset class name this issue is about, when it is about one class
            rather than the whole file.
    SetupIssue:
      type: object
      required:
        - level
        - where
        - message
      description: >-
        One thing the file's text got wrong (`error`) or one note about it
        (`warning`, such as a web address in a description).
      properties:
        level:
          type: string
          enum:
            - error
            - warning
        where:
          type:
            - string
            - 'null'
          description: Where in the file, or null for the file as a whole.
        line:
          type: integer
          description: The line of the file the issue is on, when it is known.
        message:
          type: string
    ComplianceResponse:
      type: object
      properties:
        portfolio_id:
          type: string
          format: uuid
        scenario_id:
          type: string
          format: uuid
        total_value:
          type: number
          description: Total portfolio value in base currency
        price_source:
          type: string
          enum:
            - overrides
          description: >
            Always `overrides` on this surface. `prices` is required and

            checked to cover every non-cash position before this is computed

            (a held option that owns its price is the exception, ADR-147),

            so every value here is the caller's or the person's own (ADR-036).
            The dashboard's

            `data_age_seconds` is not carried; it measures Allocait's own

            stored quote, which this call never uses.
        violation_count:
          type: integer
        violations:
          type: array
          description: |
            One entry per non-cash holding in an asset class whose total is
            outside its allocation band, each with `outside_by`, the class's
            distance from its band (ADR-113). Cash, and a class you hold
            nothing in, have no holding to list, so those breaches appear in
            `class_violations` only. A holding priced at 0 has no entry
            either (ADR-080).
          items:
            $ref: '#/components/schemas/Violation'
        class_violation_count:
          type: integer
        class_violations:
          type: array
          description: |
            One entry per asset class whose total, cash included, is outside
            its allocation band. Every class with a band is judged, including
            one you hold nothing in: it sits at 0%, so a band with a minimum
            above 0 reads BELOW_MIN (ADR-069). The same class verdict the app
            shows. A class total is the net of its holdings: a short
            subtracts, and a total below zero counts as 0 (ADR-153).
          items:
            $ref: '#/components/schemas/ClassViolation'
        size_violation_count:
          type: integer
        size_violations:
          type: array
          description: |
            One entry per non-cash holding outside its class's range for the
            side it is on, by its size without a sign: a long against the
            position size range, a short (negative `shares`) against the
            short size range (ADR-144). The same position verdict the app
            shows. A holding on a side with no range is not listed. Each
            entry carries `side`.
          items:
            $ref: '#/components/schemas/SizeViolation'
        status:
          type: string
          enum:
            - compliant
            - partial
            - violations
            - unjudged
          description: |
            The verdict in one word (ADR-073, ADR-080). `violations` when any
            class or size breach is listed. `partial` when nothing judged
            breaches but `unchecked` lists holdings no rule judged.
            `compliant` only when there is no breach and `unchecked` is
            empty. `unjudged` when nothing can be judged, for example a
            portfolio worth zero or less or a scenario with no rules. An
            `unjudged` answer lists no breach and `issue_count` is 0
            (ADR-153).
        issue_count:
          type: integer
          description: Class and size breaches together.
        unchecked:
          type: array
          description: >-
            Holdings no rule could judge, such as a holding with no asset class,
            each with the reason. A holding is judged by its class's band or by
            the size range for its side (a long by the position size, a short by
            the short size). A holding priced at 0 is judged at 0%, so it is not
            listed here.
          items:
            type: object
            properties:
              symbol:
                type: string
              pct:
                type:
                  - number
                  - 'null'
                description: >-
                  The holding as % of portfolio value. Null when the holding is
                  unpriced.
              reason:
                type: string
                enum:
                  - unclassified
                  - no_rule
                  - unpriced
    Trade:
      type: object
      properties:
        symbol:
          type: string
        asset_class:
          type: string
        action:
          type: string
          enum:
            - BUY
            - SELL
          description: |
            The direction of the amount: BUY adds shares to the holding,
            SELL removes shares. The amount is arithmetic on the user's own
            rules, not a recommendation. It is sized from the class's priced
            holdings only. Allocait's own dashboard shows a per-class
            priced_count/unpriced_count/prices_complete disclosure for a
            partially priced class (ADR-057), never carried here, for the
            same reason `Violation.status` above states (ADR-036).
        direction:
          type: string
          enum:
            - ADD
            - REDUCE
          description: >-
            Mirrors `action`, in neutral words. ADD for BUY, REDUCE for SELL.
            Added beside `action`, which keeps its values.
        current_pct:
          type: number
        target_pct:
          type: number
          description: >-
            The class's target, the same value as `class_target_pct`. Kept for
            existing callers.
        class_target_pct:
          type: number
          description: >-
            The asset class's target on its total that this amount moves toward
            -- the rule's own Target % when set, otherwise the midpoint of its
            allocation band.
        current_price:
          type: number
        shares:
          type: number
        dollar_value:
          type: number
        resulting_pct:
          type: number
    Violation:
      type: object
      properties:
        symbol:
          type: string
          example: ALFA
        asset_class:
          type: string
          example: Equities
        status:
          type: string
          enum:
            - ABOVE_MAX
            - BELOW_MIN
          description: >
            This position's class verdict, computed from the class's priced

            holdings only. Allocait's own dashboard shows a per-class

            priced_count/unpriced_count/prices_complete disclosure alongside

            this same verdict for a partially priced class (ADR-057).

            It is never carried here, because `prices` is required to cover
            every

            non-cash position before this is computed, so on this surface

            every class's own count can only ever read fully priced

            (ADR-036).
        current_pct:
          type: number
          description: Current position as % of portfolio
        min_pct:
          type: number
          description: >-
            Lower bound of the allocation band on the asset class total this
            position's class is measured against. Not a position size limit.
        max_pct:
          type: number
          description: >-
            Upper bound of the allocation band on the asset class total this
            position's class is measured against. Not a position size limit.
        current_price:
          type: number
        current_value:
          type: number
        outside_by:
          description: >-
            How far this holding's class total sits outside its band. The same
            figure on every row of one class.
          allOf:
            - $ref: '#/components/schemas/OutsideBy'
    ClassViolation:
      type: object
      description: >-
        An asset class whose total is outside its allocation band. Reports the
        breach only; no trade is suggested.
      properties:
        asset_class_id:
          type: string
          format: uuid
        asset_class:
          type: string
          example: Cash
        status:
          type: string
          enum:
            - ABOVE_MAX
            - BELOW_MIN
        current_pct:
          type: number
          description: Class total as % of portfolio
        min_pct:
          type: number
          description: Lower bound of the class allocation band
        max_pct:
          type: number
          description: Upper bound of the class allocation band
        current_value:
          type: number
        outside_by:
          $ref: '#/components/schemas/OutsideBy'
    SizeViolation:
      type: object
      description: >-
        A holding outside its class's position size range. Reports the breach
        only; no trade is suggested.
      properties:
        symbol:
          type: string
          example: ALFA
        asset_class_id:
          type: string
          format: uuid
        asset_class:
          type: string
        side:
          type: string
          enum:
            - long
            - short
          description: >-
            Which side the holding is on. It mirrors the sign of `shares` and
            decides which range was used (ADR-144).
        status:
          type: string
          enum:
            - ABOVE_MAX
            - BELOW_MIN
        current_pct:
          type: number
          description: >-
            This holding's size as % of portfolio, without a sign. A short of 6%
            reads 6.
        size_min_pct:
          type: number
          description: >-
            Lower bound of the range for this holding's side in this class (the
            position size range for a long, the short size range for a short)
        size_max_pct:
          type: number
          description: Upper bound of the range for this holding's side in this class
        current_price:
          type: number
        current_value:
          type: number
          description: Signed. Negative for a short.
        outside_by:
          $ref: '#/components/schemas/OutsideBy'
    OutsideBy:
      type: object
      description: |
        How far a listed breach sits outside its own band or range (ADR-113).
        A fact about the rule: it holds no share count, no BUY or SELL and no
        target. Amounts come from `POST /rebalance/preview`, or from the
        snapshot with `include_rebalance=true`, when a caller asks for them.
      required:
        - edge
        - pct_points
      properties:
        edge:
          type: string
          enum:
            - above_max
            - below_min
          description: Which side of the band or range the figure is on.
        pct_points:
          type: number
          description: >-
            How far outside, in percentage points of portfolio value, rounded to
            4 places.
        value:
          type: number
          description: >-
            The dollar amount that is outside, at the prices you passed, rounded
            to 2 places. Webhook payloads are worked out from Allocait's stored
            closing prices and never carry it.
  responses:
    RateLimited:
      description: |
        Rate limit exceeded: maximum 60 requests per minute per account,
        or 10,000 per UTC day (see the info block's Rate limits section).
        The per-minute refusal carries `Retry-After: 60` and the
        `X-RateLimit-*` headers; the daily refusal carries `Retry-After`
        set to the seconds until 00:00 UTC and no `X-RateLimit-*` headers.
        Neither has an `error_code`. `POST /support-requests` and
        `POST /agent-feedback` also return `429` with `error_code:
        DAILY_LIMIT_EXCEEDED` past 20 filed per rolling 24 hours; that one
        carries the `X-RateLimit-*` headers and no `Retry-After`.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: |
        Unexpected server error. `error_code` is `INTERNAL_ERROR` and
        `request_id` names this failure: quote it in `POST /agent-feedback`.
        The body never carries a schema, column or constraint name.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Internal error
            error_code: INTERNAL_ERROR
            request_id: 5b1e7c52-3f4a-4d0e-9a55-2d5cbe6f0a11
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Raw API key shown once when created in the Allocait app (Settings > API
        keys). Send it verbatim in this header; the server stores and compares
        only its SHA-256 hash. Allocait generates every key: `alc_` followed by
        64 lowercase hex characters. Keys issued before 2026-10-10 are 64 hex
        characters with no prefix and keep working. A key may carry an expiry
        (90 days or 1 year, or never). A malformed, unknown, revoked or expired
        key gets `401` with no further detail, and a malformed one is refused
        before any lookup. Keys cannot be created through this API.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.