---
title: "Conditionally sync full or changed portfolios"
method: POST
path: "/api/v2/lp/portfolios"
tags: ["Portfolio sync"]
---

# Conditionally sync full or changed portfolios

`POST /api/v2/lp/portfolios`

## Request

- Base URL: `https://capitalcompany.ai`
- URL: `https://capitalcompany.ai/api/v2/lp/portfolios`
- Auth: one of:
  - HTTP bearer
  - API key in header `x-api-key`

A read-only POST: send knownVersions from your saved portfolios. Changed portfolios return their full payload; unchanged ones appear only in unchangedEmails, so keep their saved data. Without changeCursor, scan the full roster and retain the first page’s nextCursor. With changeCursor, scan affected emails since the saved revision; retain that changeCursor and use the first incremental page’s nextCursor as windowCursor for every later page. Advance offset by limit even when portfolios is empty. Save the new cursor and apply removals only after the entire scan succeeds. Errors per LP are always returned, even for matching versions. There is no updatedSince timestamp filter: the durable change cursor tracks affected LPs. FULL_SYNC_REQUIRED asks for a full conditional scan (including on UTC day rollover); ROSTER_CHANGED asks you to restart the current scan. Required key scope: lp:read.

## Request body

- LpPortfolioSync
  - `limit` integer: Maximum roster or affected-email rows examined per page (1–25). Omitted means 10; unchanged and removed rows also count.
  - `offset` integer: Number of roster rows to skip; omitted means 0. Advance by the requested limit, not the number of changed portfolios.
  - `rosterVersion` string: Full-scan roster token returned by the first page. Send on later full-scan pages; on ROSTER_CHANGED restart at offset 0 without it. Do not combine with changeCursor.
  - `knownVersions` object[], required: Required list of saved email/version pairs (maximum 5,000); send [] for a first sync. Changed portfolios are returned in full, unchanged ones by email only.
    - `email` string, required: Email for a locally saved portfolio. Emails must be unique after normalization.
    - `version` string, required: Opaque version returned with the saved portfolio. Only successful matches are omitted; errors remain visible.
  - `changeCursor` string: Opaque nextCursor saved from a completed scan. Requests only affected emails since that revision. Omit for a full conditional scan; FULL_SYNC_REQUIRED requires a full scan.
  - `windowCursor` string: For subsequent incremental pages, send nextCursor from the first incremental page. Keep changeCursor fixed through the scan; windowCursor requires it and is mandatory for offset > 0.

## Response `200`

Same shape as GET. In incremental mode totalCount counts affected emails including removals; in full mode it counts the roster. knownVersions is required even when empty. Tokens are opaque and team-scoped.

- LpPortfolioSyncResponse
  - `success` true, required
  - `data` object, required
    - `totalCount` integer, required: Full-scan roster size or incremental affected-email count, including unchanged and removed rows.
    - `nextCursor` string, required: Opaque revision token. For a full scan retain the FIRST page’s cursor and save it only after completing all pages. For an incremental scan use it as windowCursor on later pages, then save it after the whole window succeeds.
    - `rosterVersion` string, required
    - `portfolios` union[], required
      - union
        - object
          - `email` string, required
          - `version` string, required
          - `success` true, required
          - `data` object, required
            - `lp` object, required
              - …
            - `summary` object, required
              - …
            - `funds` object[], required
              - …
            - `commitments` object[], required
              - …
            - `taxDocuments` object[], required
              - …
            - `pendingSignatures` object[], required
              - …
            - `unpaidCapitalCalls` object[], required
              - …
            - `holdings` object[], required
              - …
            - `holdingsSummary` object, required
              - …
        - object
          - `email` string, required
          - `version` string, required
          - `success` false, required
          - `error` object, required
            - `code` 'INVALID_PARAMS', required
            - `message` string, required
            - `status` integer, required
        - object
          - `email` string, required
          - `version` string, required
          - `success` false, required
          - `error` object, required
            - `code` 'LP_NOT_FOUND', required
            - `message` string, required
            - `status` integer, required
        - object
          - `email` string, required
          - `version` string, required
          - `success` false, required
          - `error` object, required
            - `code` 'NO_PORTFOLIO_DATA', required
            - `message` string, required
            - `status` integer, required
        - object
          - `email` string, required
          - `version` string, required
          - `success` false, required
          - `error` object, required
            - `code` 'FUND_NOT_FOUND', required
            - `message` string, required
            - `status` integer, required
    - `unchangedEmails` string[], required: Portfolios on this page whose successful response matches a submitted version. Keep the locally stored payload.
    - `removedEmails` string[], required: Emails no longer in the visible roster. Reconcile removals only after all pages complete successfully.
    - `pagination` object, required
      - `limit` integer, required
      - `offset` integer, required
      - `hasMore` boolean, required

## Other responses

- `400`: INVALID_JSON, INVALID_PARAMS or INVALID_CURSOR: check the body, cursor ownership and pagination constraints.
- `401`: MISSING_API_KEY or INVALID_API_KEY: missing, invalid or revoked key.
- `403`: INSUFFICIENT_SCOPE: the key lacks the required scope.
- `405`: METHOD_NOT_ALLOWED: this endpoint requires POST.
- `409`: ROSTER_CHANGED: restart the scan. FULL_SYNC_REQUIRED: complete a full conditional scan without changeCursor, then save its first-page cursor. UTC day rollover, shared valuation changes and team-wide invalidations can require this.
- `429`: RATE_LIMIT_EXCEEDED: respect Retry-After before retrying.
- `500`: INTERNAL_ERROR: unexpected server error.

## Changes

- **2026-10-19** `b99bb24f107d`: 2 warning
  - added the new `from-invested-capital` enum value to the `data/portfolios/items/anyOf[subschema #1]/data/commitments/items/feeTerms/items/collectionMethod` response property for the response status `200`
  - added the new `initial-investment` enum value to the `data/portfolios/items/anyOf[subschema #1]/data/commitments/items/feeTerms/items/earningCondition` response property for the response status `200`

[Operation history](https://skmtc.dev/capitalcompany/apis/capital-company-external-api/changes/api/v2/lp/portfolios/post.md)

---

[API](https://skmtc.dev/capitalcompany/apis/capital-company-external-api.md) · [All operations](https://skmtc.dev/capitalcompany/apis/capital-company-external-api/llms.txt) · [OpenAPI document](https://skmtc.dev/capitalcompany/apis/capital-company-external-api/revisions/b99bb24f107d?raw)
