> ## 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.

# List positions

> Returns what the user recorded for every position in a portfolio:
symbol, shares, asset class, Moves. Unpriced: no current price, value, or
day change; Allocait never returns a value computed from its own
stored quote on this surface (ADR-036). Ordered by symbol. For
values, POST /portfolio/snapshot with prices.




## OpenAPI

````yaml /openapi.yaml get /positions
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:
  /positions:
    get:
      tags:
        - Holdings and cash
      summary: List positions
      description: >
        Returns what the user recorded for every position in a portfolio:

        symbol, shares, asset class, Moves. Unpriced: no current price, value,
        or

        day change; Allocait never returns a value computed from its own

        stored quote on this surface (ADR-036). Ordered by symbol. For

        values, POST /portfolio/snapshot with prices.
      operationId: listPositions
      parameters:
        - $ref: '#/components/parameters/ListLimit'
        - $ref: '#/components/parameters/ListCursor'
        - name: portfolio_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: List of positions
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Position'
                  has_more:
                    type: boolean
        '400':
          description: '`portfolio_id` missing or not a UUID'
          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 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:
  parameters:
    ListLimit:
      name: limit
      in: query
      required: false
      description: Max rows to return. Default 100, hard max 500 (enforced server-side).
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 100
    ListCursor:
      name: cursor
      in: query
      required: false
      description: >-
        Row offset to start from. Default 0. Pass the previous cursor (or 0)
        plus limit to get the next page.
      schema:
        type: integer
        minimum: 0
        default: 0
  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:
    Position:
      type: object
      description: |
        What the user recorded for one holding: symbol, shares, asset class,
        Moves, and for an option its mark. No price, value, or day change.
        Allocait never returns a value computed from its own stored quote on
        this surface (ADR-036). For values, POST /portfolio/snapshot with
        prices.
      properties:
        id:
          type: string
          format: uuid
          description: The position row's id, as `api.position_list` returns it.
        portfolio_id:
          type: string
          format: uuid
        symbol:
          type: string
        shares:
          type: number
          description: >-
            Signed. Negative is a short (the portfolio needs Allow shorts on;
            never in Options) (ADR-153). For a holding filed under the Options
            system asset class, recorded as contracts × 100 at the per-share
            premium (ADR-056).
        position_sizing:
          type: string
          enum:
            - MANUAL
            - MINIMUM
            - BALANCED
            - MAXIMUM
        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 (-3 is an inverse
            3x fund). 1 unless the user set it, and always 1 on cash and on
            Options holdings. Allocait never infers it from a symbol (ADR-142).
            Present on every row.
        mark:
          type:
            - number
            - 'null'
          description: >-
            The per-share premium the user recorded for an option, as their
            broker quotes it (2.35 for a $235 contract). Only on a holding in
            the Options class, otherwise null. It is the user's own number,
            never Allocait's quote, and it is what the option counts at
            (ADR-147). An option with no mark is an unpriced holding.
        mark_on:
          type:
            - string
            - 'null'
          format: date
          description: >-
            The day (US Eastern) the mark was last set, changed, or confirmed by
            a trade price. Writing the same mark again keeps the date. A date to
            show, not a judgment of how old is too old. Null when there is no
            mark.
        expired_on:
          type:
            - string
            - 'null'
          format: date
          description: >-
            The option's expiry date once it has passed (from the day after, US
            Eastern), else null. An expired option counts 0, is not size-judged
            and makes no read incomplete. Allocait changes nothing on it. Close
            it with a write of `shares` 0 and `price` 0 after the user says what
            happened.
    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
    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
  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.