Kubera Data API v3
Read and write your Kubera portfolio programmatically — authenticate with an API key/secret, list and export portfolios, create and update assets and debts, manage cash flow entries, archive items, and generate recap reports.
- Base URL:
https://api.kubera.com - Path prefix:
/api/v3/data - Content type:
application/jsonon every request - Auth: HMAC-SHA256 signed headers (see Authentication)
On this page
Getting started
Profile
Portfolio endpoints
- List portfolios
- Get portfolio data
- Create portfolio
- Update portfolio
- Delete portfolio
- Portfolio CAGR
- Portfolio net worth history
- Top movers
Sheet endpoints
Section endpoints
Item endpoints
Cash flow endpoints
Fund schedule endpoints
Recap report endpoints
Reference
Endpoint index
Every endpoint requires the three authentication headers. All paths are relative to https://api.kubera.com .
| # | Group | Purpose | Method | Path | Body | Returns |
|---|---|---|---|---|---|---|
| 1 | Profile | Get profile | GET | /api/v3/data/profile |
— | Account holder details |
| 2 | Look up a ticker | GET | /api/v3/data/ticker?name={q} |
— | ticker[] |
|
| 3 | Portfolios | List portfolios | GET | /api/v3/data/portfolio |
— | portfolio[] |
| 4 | Get portfolio data | GET | /api/v3/data/portfolio/{portfolioId} |
— | Portfolio with assets, debts, totals | |
| 5 | Create portfolio | POST | /api/v3/data/portfolio |
Yes | New portfolio | |
| 6 | Update portfolio | POST | /api/v3/data/portfolio/{portfolioId} |
Yes | Updated portfolio | |
| 7 | Delete portfolio | DELETE | /api/v3/data/portfolio/{portfolioId} |
— | Success envelope. Permanent | |
| 8 | Portfolio CAGR | GET | /api/v3/data/portfolio/{portfolioId}/cagr |
— | CAGR per window + benchmarks | |
| 9 | Net worth history | GET | /api/v3/data/portfolio/{portfolioId}/history |
— | Daily net worth series | |
| 10 | Top movers | GET | /api/v3/data/portfolio/{portfolioId}/topMovers |
— | Biggest movers by day and month | |
| 11 | Sheets | Create sheet | POST | /api/v3/data/portfolio/{portfolioId}/sheet |
Yes | New sheet + seeded section |
| 12 | Update sheet | POST | /api/v3/data/sheet/{sheetId} |
Yes | Updated sheet | |
| 13 | Archive sheet | POST | /api/v3/data/sheet/{sheetId}/archive |
— | Success envelope | |
| 14 | Sections | Create section | POST | /api/v3/data/sheet/{sheetId}/section |
Yes | New section |
| 15 | Update section | POST | /api/v3/data/section/{sectionId} |
Yes | Updated section | |
| 16 | Archive section | POST | /api/v3/data/section/{sectionId}/archive |
— | Success envelope | |
| 17 | Items | Create item | POST | /api/v3/data/item |
Yes | New item ids |
| 18 | Update item | POST | /api/v3/data/item/{itemId} |
Yes | Success envelope | |
| 19 | Archive item | POST | /api/v3/data/item/{itemId}/archive |
— | Empty data |
|
| 20 | Item value history | GET | /api/v3/data/item/{itemId}/history |
— | Value series for one item | |
| 21 | Cash flow | Get cash flow | GET | /api/v3/data/item/{itemId}/cashFlow |
— | cashFlow[] |
| 22 | Insert/update cash flow | POST | /api/v3/data/item/{itemId}/cashFlow |
Yes | Success envelope | |
| 23 | Delete cash flow entry | DELETE | /api/v3/data/item/{itemId}/cashFlow/{date} |
— | Deleted entry. Permanent | |
| 24 | Fund schedule | Get fund schedule | GET | /api/v3/data/item/{itemId}/fundSchedule |
— | fundSchedule[] |
| 25 | Insert/update fund schedule | POST | /api/v3/data/item/{itemId}/fundSchedule |
Yes | Created/updated entry | |
| 26 | Delete fund schedule entry | DELETE | /api/v3/data/item/{itemId}/fundSchedule/{type}/{date} |
— | Deleted entry. Permanent | |
| 27 | Recap | Request recap report | GET | /api/v3/data/portfolio/{portfolioId}/recap/report |
— | 202 + reportId |
| 28 | Poll recap report | GET | /api/v3/data/portfolio/{portfolioId}/recap/report/{reportId} |
— | Status, then results |
{itemId} is an asset id or a debt id — the same endpoints serve both.
Authentication
Generate your keys
Generate your API key and secret from Kubera Settings > API.
- IP restrictions are optional but highly recommended.
- Keep your API key and secret confidential at all times.
- In case of accidental disclosure, delete the compromised keys.
- When creating a key, choose the appropriate permission level. The default setting is “Read portfolio data”.
- Remove any API keys that are no longer in use.
Required headers
Every request must include these three headers, plus Content-Type: application/json .
| Header | Value |
|---|---|
x-api-token |
Your API key |
x-timestamp |
Current time in seconds (Unix epoch) |
x-signature |
HMAC-SHA256 signature (see below) |
Signature generation
Build the signing string by concatenating five values in this exact order, with no separators:
{apiKey}{timestamp}{HTTP_METHOD}{requestPath}{body}
| Part | Notes |
|---|---|
apiKey |
Same value as the x-api-token header |
timestamp |
Unix epoch in seconds, same value as the x-timestamp header |
HTTP_METHOD |
Uppercase — GET or POST |
requestPath |
Path only, starting with /api/v3/.... No host, no query string |
body |
The request body serialized with compact encoding (no spaces between keys/values). Empty string for requests with no body, such as GET |
Sign that string with HMAC-SHA256 using your API secret. The hex digest is your x-signature .
The body you sign and the body you send must be byte-identical. If your HTTP client re-serializes the body (adding spaces or reordering keys), the signature will not match. Serialize once, sign that string, send that same string.
JavaScript
const crypto = require('crypto-js');
const apiKey = 'Your API Key';
const secret = 'Your API Secret';
const timestamp = Math.floor(Date.now() / 1000); // x-timestamp
const bodyData = JSON.stringify(request.body); // compact encoding
const data = `${apiKey}${timestamp}POST${request.path}${bodyData}`;
const signature = crypto.HmacSHA256(data, secret).toString(crypto.enc.Hex); // x-signature
Python
import time
import math
import hashlib
import hmac
import json
api_key = "Your API Key"
secret = "Your API Secret"
item_id = "Item ID"
request_body = {"value": 400}
timestamp = str(math.floor(time.time())) # x-timestamp
body_data = json.dumps(request_body, separators=(',', ':')) if request_body else ""
request_type = "POST"
request_path = f"/api/v3/data/item/{item_id}"
data = f"{api_key}{timestamp}{request_type}{request_path}{body_data}"
signature = hmac.new(
secret.encode('utf-8'),
data.encode('utf-8'),
hashlib.sha256
).hexdigest() # x-signature
Worked signature examples
Both examples use this sample key and secret:
apiKey = a20d0129-c121-433c-90e3-97068458584d secret = s-397a324204a34921a72c9ec7a41c22f2
Case 1 — GET (no body)
timestamp = 1726554618 body = (empty string) path = /api/v3/data/portfolio signing string: a20d0129-c121-433c-90e3-97068458584d1726554618GET/api/v3/data/portfolio signature: 8a24943d24d6def02b38fd6522a4a899883243f30e9f3bda715ddbb6345e0abf
Case 2 — POST (with body)
timestamp = 1726554715
body = {"value":400}
path = /api/v3/data/item/27f59377-44e8-4a94-ac3b-f1e1c5080a48
signing string:
a20d0129-c121-433c-90e3-97068458584d1726554715POST/api/v3/data/item/27f59377-44e8-4a94-ac3b-f1e1c5080a48{"value":400}
signature:
584977d5822c3bf697ce929cc86ef4ac41331edba4f3d3c55d0d67361b76542e
Use these to verify your signing implementation before making live calls — if you reproduce both digests exactly, your signing is correct.
Response format
Every response uses the same envelope:
{
"data": ...,
"errorCode": 0
}
| Field | Description |
|---|---|
data |
The payload. Shape depends on the endpoint; {} when the endpoint returns nothing |
errorCode |
0 on success |
Conventions
| Convention | Detail |
|---|---|
| Ids | UUID strings. Portfolio, item, section and sheet ids are all opaque — pass them back exactly as received |
| Dates | YYYY-MM-DD strings |
| Timestamps | x-timestamp is in seconds. Recap createdAt / updatedAt are in milliseconds |
| Monetary fields | Returned as a Value object — { "amount": number, "currency": string } — not a bare number |
| Currency | ISO codes, e.g. USD |
| Item ids | An asset id and a debt id are interchangeable wherever {itemId} appears |
API limits
There are different types of limits, all of which are subject to change at any time.
| Limit | Threshold |
|---|---|
| Rate | 600 requests per minute |
| Kubera Essential | 6000 requests per day (UTC) |
| Kubera Black | 24000 requests per day (UTC) |
| Kubera for Business | 24000 requests per day (UTC) |
Endpoints added for parity with Kubera's AI tools — Profile, ticker lookup, the portfolio create/update/delete and analytics endpoints, and everything under Sheets, Sections and Fund schedule — are counted against a separate daily allowance of the same size, rather than sharing the one above. The per-minute rate limit is shared.
Get profile
GET /api/v3/data/profile
Returns the account holder's own details. Useful for confirming which account an API key belongs to, and for reading the time zone Kubera uses when it rolls up daily values.
curl --location --request GET 'https://api.kubera.com/api/v3/data/profile' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Response
{
"data": {
"name": "Ada Lovelace",
"utcOffset": -330,
"tz": "Asia/Kolkata",
"dateOfBirth": ""
},
"errorCode": 0
}
utcOffset is in minutes. dateOfBirth is an empty string when it has not been set.
Look up a ticker
GET /api/v3/data/ticker
Resolves a name or symbol to Kubera's tickers. Call this before sending a ticker to Create item — the same symbol exists on several exchanges and as different instrument types, so pick the match whose type / subType , market / code and currency fit the holding you mean.
curl --location --request GET 'https://api.kubera.com/api/v3/data/ticker?name=apple' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Name or symbol to search for |
Response
{
"data": [
{
"id": 4321,
"ticker": "AAPL",
"name": "Apple Inc",
"type": "stock",
"subType": "Common Stock",
"code": "AAPL",
"market": "NASDAQ",
"currency": "USD",
"countryCode": "US"
}
],
"errorCode": 0
}
Exact matches come first, and at most 10 matches are returned. An empty array simply means nothing matched. Each entry is a Ticker object; pass its ticker value back when creating an item.
List portfolios
GET /api/v3/data/portfolio
Fetch the list of portfolios. Start here — every other endpoint needs a portfolioId or an itemId that originates from this call.
curl --location --request GET 'https://api.kubera.com/api/v3/data/portfolio' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Response
{
"data": [
{
"id": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"name": "Mike",
"currency": "USD"
}
],
"errorCode": 0
}
data is an array of Portfolio objects.
Get portfolio data
GET /api/v3/data/portfolio/{portfolioId}
Fetch a particular portfolio’s full data.
Path parameters
| Parameter | Required | Description |
|---|---|---|
portfolioId |
Yes | From the List portfolios response |
curl --location --request GET 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Response
{
"data": {
"id": "Portfolio id",
"name": "Portfolio name",
"ticker": "Portfolio currency",
"timestamp": "Time",
"asset": [],
"debt": [],
"document": [],
"insurance": [],
"totalAssets": { "amount": 0, "currency": 0 },
"totalDebts": { "amount": 0, "currency": 0 },
"netWorth": { "amount": 0, "currency": 0 },
"costBasis": 0,
"unrealizedGain": 0,
"allocationByAssetClass": {
"Cash": 0,
"Crypto": 0,
"Stock": 0,
"Fund": 0,
"Derivative": 0,
"Investment": 0
}
},
"errorCode": 0
}
| Field | Type | Description |
|---|---|---|
asset |
array | Asset objects |
debt |
array | Debt objects — same shape as assets |
insurance |
array | Insurance objects — same shape as assets |
document |
array | Document objects |
totalAssets, totalDebts, netWorth |
object | Value objects |
costBasis, unrealizedGain |
number | — |
allocationByAssetClass |
object | Allocation per asset class |
Additional fields may be present — parse defensively and ignore unknown keys.
The id values inside asset and debt are the {itemId} used by the item and cash flow endpoints.
Create portfolio
POST /api/v3/data/portfolio
Creates an empty portfolio, pre-seeded with Kubera's default sheets and sections (Investments, Real Estate, Others, Credit Cards, Loans, and so on). Call Get portfolio data on the new portfolio to see what it was given before adding sheets of your own.
curl --location --request POST 'https://api.kubera.com/api/v3/data/portfolio' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"name": "Family Trust",
"currency": "USD"
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Portfolio name |
currency |
string | No | Reporting currency, e.g. USD. Defaults to the currency inferred for the account. Ignored for Kubera for Business users — those portfolios always take the organisation's currency, so read currency back off the response rather than assuming the value you sent |
Response
{
"data": {
"id": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"name": "Family Trust",
"currency": "USD"
},
"errorCode": 0
}
Errors — 400 for an invalid currency; 500 Maximum portfolio limit reached when the plan caps portfolio count.
Update portfolio
POST /api/v3/data/portfolio/{portfolioId}
Renames a portfolio and/or changes its reporting currency. This is the portfolio itself — to change an asset or debt inside it use Update item.
Changing
currencyre-denominates the entire portfolio and makes Kubera recompute returns and rebuild its charts, which takes a while to settle. Send it only when the reporting currency really is changing. It is not how you record an item held in another currency — set the currency on the item instead.
curl --location --request POST 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"name": "Family Trust (2026)"
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | No | New portfolio name |
currency |
string | No | New reporting currency. Must be a fiat currency |
At least one of the two is required.
Response
{
"data": {
"id": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"name": "Family Trust (2026)",
"currency": "USD"
},
"errorCode": 0
}
Errors — 400 Nothing to update when both fields are absent; 400 for an invalid or non-fiat currency.
Delete portfolio
DELETE /api/v3/data/portfolio/{portfolioId}
This is permanent. It deletes the portfolio and everything in it — every sheet, section, asset, debt, insurance policy, document, cash flow entry, and the complete value history of all of them. Nothing is archived and nothing is recoverable afterwards. To remove only part of a portfolio, archive the individual sheets, sections or items instead — archiving preserves their history so past net worth stays correct.
curl --location --request DELETE 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
portfolioId |
string | Portfolio to delete permanently |
Response
{
"data": {
"success": true,
"message": "Portfolio deleted successfully"
},
"errorCode": 0
}
Errors — 400 Cannot delete the only portfolio in the account .
Portfolio CAGR
GET /api/v3/data/portfolio/{portfolioId}/cagr
Compound annual growth rate for the portfolio, over each of Kubera's standard windows, for both net worth and investable assets — plus the same-period return of a set of market benchmarks to compare against.
curl --location --request GET 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>/cagr' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
portfolioId |
string | Portfolio to report on |
Response
{
"data": {
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"portfolioName": "Personal",
"portfolioCurrency": "USD",
"portfolioCurrencyTickerId": 171,
"portfolioStartDate": "2021-03-14",
"isCustomStartDate": false,
"cagrPercentages": {
"ytd_networth": 7.42,
"ytd_investable": 9.11,
"qtd_networth": 2.03,
"qtd_investable": 2.58,
"yearly_networth": 11.87,
"yearly_investable": 14.02,
"alltime_networth": 9.65,
"alltime_investable": 12.30
},
"cagrOldValues": {
"ytd_networth": { "oldValue": 1250000, "date": "2026-01-01" },
"alltime_networth": { "oldValue": 480000, "date": "2021-03-14" }
},
"market": {
"GSPC.INDX": 10.44,
"IXIC.INDX": 13.91,
"BTC.CC": 22.07
}
},
"errorCode": 0
}
Field notes
| Field | Description |
|---|---|
cagrPercentages |
CAGR as a percentage for each window. Eight keys: ytd, qtd, yearly and alltime, each as _networth and _investable. Present only once the portfolio has data for today |
cagrOldValues |
The starting value each window was measured from, as { oldValue, date }. Same eight keys. Always present |
market |
Benchmark returns over the same span as alltime, keyed by index symbol (GSPC.INDX, BTC.CC, …). null when the portfolio has no start date. A benchmark that cannot be priced is omitted, so treat every key as optional |
portfolioStartDate |
Where the all-time window begins — YYYY-MM-DD, or null if the portfolio has no history yet |
Portfolio net worth history
GET /api/v3/data/portfolio/{portfolioId}/history
The portfolio's net worth over time, one data point per day, oldest first. This is the series behind the net worth chart in the app.
curl --location --request GET 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>/history' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
portfolioId |
string | Portfolio to report on |
Response
{
"data": {
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"portfolioName": "Personal",
"portfolioCurrency": "USD",
"portfolioCurrencyTickerId": 171,
"portfolioStartDate": "2021-03-14",
"isCustomStartDate": false,
"portfolioDataPoints": [
{
"date": "2026-09-26",
"value": 1342500,
"investibleTotal": 910400,
"assetTotal": 1520000,
"debtTotal": 177500
}
]
},
"errorCode": 0
}
value is net worth — assetTotal minus debtTotal — in the portfolio currency. portfolioDataPoints is an empty array when the portfolio has no history yet.
Top movers
GET /api/v3/data/portfolio/{portfolioId}/topMovers
The items whose value moved most over the last day and the last month, assets and debts ranked separately, largest change first.
curl --location --request GET 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>/topMovers' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
portfolioId |
string | Portfolio to report on |
Response
{
"data": {
"change": {
"day": {
"baseDate": "2026-09-27",
"currentDate": "2026-09-28",
"asset": [
{
"name": "Apple",
"value": 24100,
"currentValue": 24850,
"archived": 0
}
],
"assetChangeTotal": 750,
"assetTotal": 1520000,
"debt": [],
"debtChangeTotal": 0,
"debtTotal": 177500
},
"month": { "...": "same shape, compared against a month ago" },
"week": null,
"year": null,
"lastYearEnd": null
},
"currency": "USD",
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"portfolioName": "Personal",
"tickerId": 171,
"tsCreated": 1790000000
},
"errorCode": 0
}
Only day and month are populated. The week , year and lastYearEnd keys are always present but unset on this endpoint. A window is also unset when the portfolio is younger than it.
Within a window, value is the item's value on baseDate and currentValue is its value today, both in the portfolio currency; archived is 1 for an item archived since. tsCreated is in seconds.
Create sheet
POST /api/v3/data/portfolio/{portfolioId}/sheet
A sheet is a top-level tab that holds sections; sections hold the items. A sheet belongs to exactly one category and can only hold items of that category, so choose it deliberately.
A new sheet arrives with one empty section already in it (“Section 1”), so you can add items straight away. Use Create section only when you want additional sections.
Insurancecannot be created here. A portfolio is given its single Insurance sheet when the portfolio itself is created, and there is no way to add another. Update sheet still acceptsInsurance, for that existing sheet.
curl --location --request POST 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>/sheet' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"name": "Private Equity",
"category": "Asset",
"updateFrequency": 3
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Sheet name |
category |
string | No | Asset (default) or Debt. Case-insensitive |
updateFrequency |
number | No | How often Kubera reminds you to refresh manually-valued items on this sheet. 0 never (default), 1 weekly, 6 fortnightly, 2 monthly, 3 quarterly, 4 half-yearly, 5 yearly |
Response
{
"data": {
"id": "5c2f0e21-7a44-4c0e-9a1e-2b6f0d3c8a19",
"name": "Private Equity",
"category": "Asset",
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"sortKey": "3",
"updateFrequency": 3,
"tsArchive": 0,
"sections": [
{
"id": "9fcbee08-3524-470f-9352-ba99f0f6cf37",
"name": "Section 1",
"sheetId": "5c2f0e21-7a44-4c0e-9a1e-2b6f0d3c8a19",
"sortKey": "0",
"expanded": 1,
"tsArchive": 0
}
]
},
"errorCode": 0
}
sortKey is assigned automatically, appending the sheet after the existing sheets in its category. See the Sheet object.
Errors — 400 "category" must be one of [Asset, Debt] .
Update sheet
POST /api/v3/data/sheet/{sheetId}
Renames a sheet, reorders it, or changes its category or reminder cadence. Send only the fields you want changed; anything omitted keeps its current value.
Changing
categorymoves the whole sheet — and every item on it — between the asset, debt and insurance sides of the portfolio. That flips whether those items count towards net worth as owned or owed. A sheet cannot be moved to a different portfolio.
curl --location --request POST 'https://api.kubera.com/api/v3/data/sheet/<SHEET_ID>' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"name": "Private Markets",
"sortKey": "1"
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | No | New sheet name |
category |
string | No | Asset, Debt or Insurance |
updateFrequency |
number | No | Same values as Create sheet |
sortKey |
string | No | Numeric string positioning the sheet among the sheets of its category. "0" is first |
At least one field is required.
Response
{
"data": {
"id": "5c2f0e21-7a44-4c0e-9a1e-2b6f0d3c8a19",
"name": "Private Markets",
"category": "Asset",
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"sortKey": "1",
"updateFrequency": 3,
"tsArchive": 0
},
"errorCode": 0
}
The response is the updated sheet without its sections .
Errors — 400 Nothing to update ; 400 Invalid sheetId/Archived Sheet .
Archive sheet
POST /api/v3/data/sheet/{sheetId}/archive
Archives a sheet and everything under it — all of its sections and all of their items. This is exactly what the app's delete-sheet action does: the sheet stops appearing and stops counting towards net worth, while past values are preserved so historical net worth stays correct.
A sheet can only be un-archived from the Kubera app, not through this API.
curl --location --request POST 'https://api.kubera.com/api/v3/data/sheet/<SHEET_ID>/archive' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
sheetId |
string | Sheet to archive |
Response
{
"data": {
"success": true,
"message": "Sheet archived successfully"
},
"errorCode": 0
}
Errors — 400 Invalid sheetId/Archived Sheet , for an unknown sheet or one already archived.
Create section
POST /api/v3/data/sheet/{sheetId}/section
A section is the group that actually holds assets and debts, so a sheet needs at least one before anything can go in it. The section inherits its sheet's category.
Once it exists, put items in it by passing the sheet and section names to Create item's sheetName / sectionName . You only need this endpoint to create a section up front — Create item also makes one on demand when its sheetName matches a sheet and its sectionName matches nothing in it.
curl --location --request POST 'https://api.kubera.com/api/v3/data/sheet/<SHEET_ID>/section' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"name": "Venture Funds",
"expanded": 1
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Section name |
expanded |
number | No | Whether the section renders expanded in the app: 1 (default) or 0. Cosmetic only |
Response
{
"data": {
"id": "9fcbee08-3524-470f-9352-ba99f0f6cf37",
"name": "Venture Funds",
"sheetId": "5c2f0e21-7a44-4c0e-9a1e-2b6f0d3c8a19",
"sortKey": "2",
"expanded": 1,
"columnSortKey": "",
"columnSortOrder": "",
"tsArchive": 0
},
"errorCode": 0
}
See the Section object.
Errors — 400 Invalid sheetId/Archived Sheet .
Update section
POST /api/v3/data/section/{sectionId}
Renames a section, reorders it, collapses or expands it, or moves it to another sheet in the same portfolio — its items move with it. Send only the fields you want changed.
Moving a section to a sheet of a different category changes whether its items count as owned or owed. A sheet in a different portfolio is rejected.
curl --location --request POST 'https://api.kubera.com/api/v3/data/section/<SECTION_ID>' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"name": "Venture Capital",
"expanded": 0
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | No | New section name |
sortKey |
string | No | Numeric string positioning the section within its sheet |
expanded |
number | No | 1 or 0 |
sheetId |
string | No | Move the section into this sheet. Must be in the same portfolio |
At least one field is required.
Response
{
"data": {
"id": "9fcbee08-3524-470f-9352-ba99f0f6cf37",
"name": "Venture Capital",
"sheetId": "5c2f0e21-7a44-4c0e-9a1e-2b6f0d3c8a19",
"sortKey": "2",
"expanded": 0,
"columnSortKey": "",
"columnSortOrder": "",
"tsArchive": 0
},
"errorCode": 0
}
Errors — 400 Nothing to update ; 400 Cannot move a section to a sheet in another portfolio ; 400 Invalid sectionId/Archived Section .
Archive section
POST /api/v3/data/section/{sectionId}/archive
Archives a section and every item in it, preserving past values the same way Archive sheet does. To archive a single item instead, use Archive item.
Un-archiving is only possible in the Kubera app.
curl --location --request POST 'https://api.kubera.com/api/v3/data/section/<SECTION_ID>/archive' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
sectionId |
string | Section to archive |
Response
{
"data": {
"success": true,
"message": "Section archived successfully"
},
"errorCode": 0
}
Errors — 400 Invalid sectionId/Archived Section .
Create item
POST /api/v3/data/item
Create a new manual asset or debt in a portfolio. The item is added to the portfolio’s first Asset section (or first Debt section when isDebt is true ), unless sheetName / sectionName match an existing section.
Ticker-backed item — value is the quantity held:
curl --location --request POST 'https://api.kubera.com/api/v3/data/item' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"portfolioId": "<PORTFOLIO_ID>",
"name": "Apple",
"ticker": "AAPL",
"value": 10,
"cost": 1500
}'
Cash / manually-valued item — pass currency instead of ticker (or neither, to use the portfolio currency):
curl --location --request POST 'https://api.kubera.com/api/v3/data/item' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"portfolioId": "<PORTFOLIO_ID>",
"name": "Savings",
"currency": "USD",
"value": 5000
}'
value is overloaded — read this before sending
| Item type | What value means |
|---|---|
ticker is set — stock, ETF, fund, crypto, precious metal |
The quantity held (shares/units), not a currency amount. Kubera derives market value as quantity × price |
No ticker — cash or manually-valued item |
The monetary amount in the item’s currency |
cost is always a monetary amount (a total, not a per-unit price).
Sending {"ticker": "AAPL", "value": 1500} creates 1,500 shares of Apple, not $1,500 of Apple.
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
portfolioId |
string | Yes | Portfolio to add the item to |
name |
string | Yes | Item name |
value |
number | Yes | Quantity (ticker items) or amount (cash items) |
ticker |
string | No | Ticker symbol, e.g. AAPL, BTC. When set, value is the quantity |
currency |
string | No | Currency code for a cash item, e.g. USD. Ignored when ticker is set. Defaults to the portfolio currency |
cost |
number | No | Cost basis as a monetary amount |
description |
string | No | — |
isDebt |
boolean | No | true to create a debt. Defaults to false (asset) |
sheetName |
string | No | Target an existing section by sheet name (use with sectionName) |
sectionName |
string | No | Target an existing section by name |
costCurrency |
string | No | Currency for cost. Fiat only; falls back to the item's currency, then the portfolio's |
ownership |
number | No | Percentage of the item the user owns — the app's “Portfolio Share”. 0–999, defaults to 100. Values above 100 are allowed, for a geared position |
cmtdCap |
number | No | Committed capital, as a monetary amount. Never negative. Pairs with the fund schedule |
parentId |
string | No | Create the item as a holding inside this existing item — e.g. a stock inside a brokerage account. See the note below |
symbol |
string | No | Deprecated alias for ticker, used only when ticker is absent |
portfolioShare and share are accepted as aliases for ownership , in that order of precedence. Prefer ownership .
cmtdCapandownershipapply to top-level items only. Setting either on a holding is rejected: committed capital rolls up into the parent item, and a holding's ownership is read from its parent — so set them on the parent instead.
parentIdturns a plain parent into a manual account whose value becomes the sum of its holdings. It rejects connected accounts, items that are already holdings, a parent in another portfolio, and ticker-backed plain items.sheetName,sectionNameandisDebtare ignored when it is set, since the holding inherits its parent's placement.
Response
{
"data": {
"success": true,
"itemId": "cust-abc",
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"sectionId": "9fcbee08-3524-470f-9352-ba99f0f6cf37"
},
"errorCode": 0
}
Update item
POST /api/v3/data/item/{itemId}
Update a particular asset or debt.
Path parameters
| Parameter | Required | Description |
|---|---|---|
itemId |
Yes | Asset id or debt id from the Get portfolio data response |
Update the current value
curl --location --request POST 'https://api.kubera.com/api/v3/data/item/<ASSET_ID/DEBT_ID>' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"value": 400
}'
Update a historical value — include date :
curl --location --request POST 'https://api.kubera.com/api/v3/data/item/<ASSET_ID/DEBT_ID>' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"value": 350,
"date": "2026-01-01"
}'
Body properties
| Property | Type | Description |
|---|---|---|
name |
string | — |
description |
string | — |
value |
number | Follows the same quantity-vs-amount rule as Create item |
cost |
number | Monetary amount |
date |
string | YYYY-MM-DD. Writes the value at that historical date instead of today |
costCurrency |
string | Currency for cost. Fiat only; falls back to the item's currency, then the portfolio's |
ownership |
number | Percentage of the item the user owns — the app's “Portfolio Share”. 0–999, defaults to 100. Values above 100 are allowed, for a geared position |
cmtdCap |
number | Committed capital, as a monetary amount. Never negative. Pairs with the fund schedule |
portfolioShare and share are accepted as aliases for ownership , in that order of precedence. Prefer ownership .
cmtdCapandownershipapply to top-level items only. Setting either on a holding is rejected: committed capital rolls up into the parent item, and a holding's ownership is read from its parent — so set them on the parent instead.
Archive item
POST /api/v3/data/item/{itemId}/archive
Archive a single manual asset or debt. No request body is required.
Path parameters
| Parameter | Required | Description |
|---|---|---|
itemId |
Yes | Asset id or debt id from the Get portfolio data response |
Behavior depends on the item
| Item | Result |
|---|---|
| A holding (belongs to a parent account) | Its value is zeroed. The parent and sibling holdings are untouched |
| A top-level account | The account is archived |
| A connected/linked account (bank, brokerage, crypto, etc.) | Rejected. Only manually-added items can be archived |
curl --location --request POST 'https://api.kubera.com/api/v3/data/item/<ASSET_ID/DEBT_ID>/archive' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Response
{
"data": {},
"errorCode": 0
}
Get item value history
GET /api/v3/data/item/{itemId}/history
The recorded value of a single item over time, oldest first. For a ticker-backed holding this is its market value; for a manually-valued item it is the values you have entered.
curl --location --request GET 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/history' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
itemId |
string | Item whose history to fetch |
Response
{
"data": {
"itemId": "cust-abc",
"itemName": "Apple",
"itemSymbol": "AAPL",
"itemCurrency": "USD",
"itemCurrencyTickerId": 171,
"itemDataPoints": [
{ "date": "2026-09-26", "value": 24100, "quantity": 100 },
{ "date": "2026-09-27", "value": 24850, "quantity": 100 }
]
},
"errorCode": 0
}
value is the market value in the item's currency. quantity is the raw stored figure — the number of units for a ticker-backed holding — and is null where none was recorded on or before that date. itemSymbol is an empty string for an item with no ticker.
Get cash flow entries
GET /api/v3/data/item/{itemId}/cashFlow
Fetch the list of cash flow entries for an item.
curl --location --request GET 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/cashFlow' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Response
{
"data": [
{
"cashIn": 300,
"cashOut": null,
"currency": "USD",
"date": "2025-02-02",
"note": "Initial investment"
}
],
"errorCode": 0
}
data is an array of cash flow objects.
Insert/update cash flow entry
POST /api/v3/data/item/{itemId}/cashFlow
Add a cash flow entry to an item, or update the existing entry for that date.
curl --location --request POST 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/cashFlow' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"cashIn": 100.0,
"cashOut": 200.0,
"date": "2025-04-02",
"currency": "USD",
"note": "3rd investment"
}'
Body properties
| Property | Type | Description |
|---|---|---|
cashIn |
number | Money in |
cashOut |
number | Money out |
date |
string | YYYY-MM-DD |
currency |
string | ISO currency code |
note |
string | Free-text note |
Delete cash flow entry
DELETE /api/v3/data/item/{itemId}/cashFlow/{date}
This is permanent. It removes the single cash flow entry the item holds on that date, and cannot be undone.
The entry is addressed by date rather than by a row id: an item holds at most one entry per date — the same way Insert/update cash flow entry addresses one — and Get cash flow entries returns no id to quote back.
One entry per call; there is no bulk form. To clear an item's whole cash flow, delete each entry in turn, or do it in the app.
Kubera recomputes the item's IRR from the entries that remain, so deleting one changes its reported return. Deleting the last entry leaves the item with no cash-flow-based return — it is not switched back to a cost-basis IRR. This matches what the app itself does.
curl --location --request DELETE 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/cashFlow/2025-04-02' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
itemId |
string | Item the entry belongs to |
date |
string | YYYY-MM-DD, as returned by Get cash flow entries |
Response
{
"data": {
"success": true,
"deleted": true,
"date": "2025-04-02",
"cashIn": 100,
"cashOut": 200,
"currency": "USD",
"note": "3rd investment",
"message": "Cash flow entry deleted successfully"
},
"errorCode": 0
}
The response echoes the entry that was removed, so you can report what went.
Errors — 400 No cash flow entry on <date> for this item . The message also names the dates that do have entries, so a caller can correct itself.
Get fund schedule
GET /api/v3/data/item/{itemId}/fundSchedule
Returns an item's dated capital calls and distributions — typically for a private equity or other capital-commitment holding, alongside the item's committed capital (cmtdCap ).
These entries are a schedule, not money that has moved. Kubera does not use them to compute IRR, returns, net worth or the item's value; nothing here changes a reported number. Actual money in and out lives in cash flow, and that is what drives IRR. An item may have both, and they will not agree if a call was scheduled but not yet paid.
curl --location --request GET 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/fundSchedule' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
itemId |
string | The item |
Response
{
"data": {
"fundSchedule": [
{
"type": "capitalcall",
"date": "2026-01-15",
"value": 50000,
"currency": "USD",
"note": "Call 3 of 8"
},
{
"type": "distribution",
"date": "2026-06-30",
"value": 12000,
"currency": "USD",
"note": ""
}
]
},
"errorCode": 0
}
An empty array simply means no entries have been recorded. Each entry is a Fund schedule object.
Insert/update fund schedule entry
POST /api/v3/data/item/{itemId}/fundSchedule
Adds a capital call or distribution to an item, or updates the existing entry of that type on that date.
The key is type and date together: an item may hold one capital call and one distribution on the same date, but not two of the same type. Writing a distribution therefore never disturbs a capital call on that date. When an entry of the same type already exists on that date it is updated in place — value is replaced, not added to — and fields you leave out keep their stored values.
curl --location --request POST 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/fundSchedule' \
--header 'Content-Type: application/json' \
--header 'x-api-token: <API_KEY>' \
--header 'x-timestamp: <TIMESTAMP>' \
--header 'x-signature: <SIGNATURE>' \
--data '{
"type": "capitalcall",
"date": "2026-01-15",
"currency": "USD",
"value": 50000,
"note": "Call 3 of 8"
}'
Body properties
| Property | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | capitalcall or distribution |
date |
string | Yes | YYYY-MM-DD. May be future- or past-dated |
currency |
string | Yes | Currency of value |
value |
number | No | Positive for both types — the direction comes from type, not the sign |
note |
string | No | Short note, e.g. Call 3 of 8 |
Response
{
"data": {
"success": true,
"created": false,
"type": "capitalcall",
"date": "2026-01-15",
"value": 50000,
"currency": "USD",
"note": "Call 3 of 8",
"previous": {
"value": 45000,
"note": "Call 3 of 8 (provisional)"
}
},
"errorCode": 0
}
created is true when a new entry was inserted and false when an existing one was updated. previous appears only on an update, carrying the values that were overwritten.
Errors — 400 Invalid type: <x>. Expected capitalcall or distribution ; 400 Invalid currency: <x> .
Delete fund schedule entry
DELETE /api/v3/data/item/{itemId}/fundSchedule/{type}/{date}
This is permanent. It removes the single entry of that type on that date. Because the same date may hold one of each type, the type decides which of the two goes — passing the wrong one removes the wrong entry.
One entry per call; there is no bulk form.
No computed figure changes as a result. Any cash flow recorded for the same event is separate and is not removed by this — use Delete cash flow entry for that.
curl --location --request DELETE 'https://api.kubera.com/api/v3/data/item/<ASSET_ID>/fundSchedule/capitalcall/2026-01-15' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Path parameters
| Parameter | Type | Description |
|---|---|---|
itemId |
string | Item the entry belongs to |
type |
string | capitalcall or distribution |
date |
string | YYYY-MM-DD, as returned by Get fund schedule |
Response
{
"data": {
"success": true,
"deleted": true,
"type": "capitalcall",
"date": "2026-01-15",
"value": 50000,
"currency": "USD",
"note": "Call 3 of 8",
"message": "Fund schedule entry deleted successfully"
},
"errorCode": 0
}
Errors — 400 No <type> entry on <date> for this item . The message also names the entries that do exist, as type date pairs.
Recap reports
A recap report can span years of history across every holding in a portfolio, so it is too slow to compute inside a single HTTP request. The API is therefore request-then-poll:
| Step | Endpoint | Returns |
|---|---|---|
| 1. Submit | GET /api/v3/data/portfolio/{portfolioId}/recap/report |
202 with a reportId |
| 2. Poll | GET /api/v3/data/portfolio/{portfolioId}/recap/report/{reportId} |
200 with status, and the results once complete |
Only one recap computation per user runs at a time.
Request recap report
GET /api/v3/data/portfolio/{portfolioId}/recap/report
Queues the computation and returns the id of the report to poll for.
curl --location --request GET \ 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>/recap/report?reports=networth,asset_classes&timeRanges=monthly&reportType=totals&dataPoints=12¤cy=USD' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Query parameters
| Parameter | Required | Description |
|---|---|---|
reports |
Yes | Comma-separated report IDs. Case-insensitive |
timeRanges |
Yes | Comma-separated time ranges. Case-insensitive |
reportType |
Yes | totals or percentageAllocation |
dataPoints |
Yes | Integer ≥ 1. Caps the report to the most recent N points per series. quarterly and today are derived from the raw series, so the number of points actually returned for those ranges can differ |
currency |
No | ISO currency code, e.g. USD. Every value in the report is converted to it. Defaults to the portfolio’s own currency. An unknown code returns 400 |
Note: the query string is not part of the signing string — sign the path only. See Signature generation.
Response — 202 Accepted
{
"data": {
"reportId": "7c1f0a3d9b6e42f58ad0c4e91b73d2a6f5c8e1097b4d6a3f2e8c5b91d7a0463f",
"status": "pending",
"pollIntervalMs": 2000
},
"errorCode": 0
}
The response also carries a Retry-After: 2 header. status is pending for a newly queued report, or processing / completed when the same request was already requested and is still cached.
Response — another report is already running
If a different recap request is already in flight, the request returns 202 without queuing anything:
{
"data": {
"status": "in_progress",
"message": "A recap computation is already in progress for this user. Please retry shortly.",
"retryAfterMs": 2000
},
"errorCode": 0
}
There is no reportId in this response — retry the request after the suggested interval. These retries do not consume your API quota.
Agent note: branch on the presence of reportId , not on status . No reportId means nothing was queued and you must request it again.
Poll recap report
GET /api/v3/data/portfolio/{portfolioId}/recap/report/{reportId}
Fetch the state — and, once finished, the contents — of a report you requested.
curl --location --request GET \ 'https://api.kubera.com/api/v3/data/portfolio/<PORTFOLIO_ID>/recap/report/<REPORT_ID>' \ --header 'Content-Type: application/json' \ --header 'x-api-token: <API_KEY>' \ --header 'x-timestamp: <TIMESTAMP>' \ --header 'x-signature: <SIGNATURE>'
Statuses
| Status | Meaning | Next action |
|---|---|---|
pending |
Queued, not started yet | Keep polling |
processing |
Being computed | Keep polling |
completed |
Finished — params and results are present |
Read results |
failed |
The computation failed. The response carries an error message |
Submit again |
Response — still running
{
"data": {
"reportId": "7c1f0a3d9b6e42f58ad0c4e91b73d2a6f5c8e1097b4d6a3f2e8c5b91d7a0463f",
"status": "processing",
"createdAt": 1756612800000,
"updatedAt": 1756612812000
},
"errorCode": 0
}
createdAt and updatedAt are Unix timestamps in milliseconds.
Response — completed
{
"data": {
"reportId": "7c1f0a3d9b6e42f58ad0c4e91b73d2a6f5c8e1097b4d6a3f2e8c5b91d7a0463f",
"status": "completed",
"createdAt": 1756612800000,
"updatedAt": 1756612830000,
"params": {
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"currency": "USD",
"reportType": "totals",
"reports": ["asset_classes"],
"timeRanges": ["weekly"],
"dataPoints": 2
},
"results": [
{
"timeRange": "weekly",
"report": "asset_classes",
"currency": "USD",
"valueType": "number",
"rows": [
{
"label": "Stocks",
"category": "asset",
"dataPoints": [
{ "date": "2026-08-22", "value": 4050112.88 },
{ "date": "2026-08-29", "value": 4126069.16 }
],
"children": [
{
"label": "Apple Inc",
"category": "asset",
"itemId": "27f59377-44e8-4a94-ac3b-f1e1c5080a48",
"sectionId": "0f0b3a2e-6d51-4e42-9f0e-6d51c1a2b3c4",
"portfolioId": "6eb1ac79-2ae1-49e6-aada-3a5fb4fdce55",
"isArchived": false,
"dataPoints": [
{ "date": "2026-08-22", "value": 878220.45 },
{ "date": "2026-08-29", "value": 892401.00 }
]
}
]
}
]
}
]
},
"errorCode": 0
}
Reading results
results is flat: one entry per report × time range, so its length equals reports × timeRanges . A combination that produced no data still appears, with "rows": [] .
Each entry is a Recap Series object; each row is a Recap Row object.
Report IDs
Valid values for the reports query parameter.
| Report ID | What it shows |
|---|---|
networth |
Total net worth over time |
sheets_and_sections |
Breakdown by your portfolio sheets and sections |
asset_classes |
Assets by class (stocks, real estate, crypto, …) |
investable |
Investable assets |
investable_without_cash |
Investable assets excluding cash |
investable_by_sheets_and_sections |
Investable assets within each sheet and section |
investable_without_cash_by_sheets_and_sections |
Same, excluding cash |
cash_on_hand |
Cash holdings over time |
assets_and_currency |
Fiat assets by currency |
stocks_and_geography |
Stocks by geography |
stocks_and_sector |
Stocks by sector |
stocks_and_marketcap |
Stocks by market cap tier |
crypto |
Crypto holdings |
brokerages |
Holdings by brokerage / institution |
taxable_assets |
Assets by tax treatment (taxable, tax-deferred, tax-free) |
Time ranges
Valid values for the timeRanges query parameter: today , daily , weekly , monthly , quarterly , yearly .
Each range determines which historical snapshot represents a period — weekly keeps the Saturday point, monthly the last point of the calendar month, yearly the last point of the year.
Report types
Valid values for the reportType query parameter.
reportType |
Result |
|---|---|
totals |
valueType: "number" — each value is an amount in currency |
percentageAllocation |
valueType: "percentage" — each value is a rounded share of the total, with the unrounded figure in preciseValue |
Object reference
Primitives first, then the composite objects that use them.
Value object
| Field | Description |
|---|---|
amount |
Numeric amount |
currency |
Currency code |
Rate object
| Field | Description |
|---|---|
currency |
Currency |
price |
Price |
Connection object
| Field | Description |
|---|---|
accountId |
Account id |
aggregator |
Aggregator name |
id |
Connection id |
lastUpdatedTimestamp |
Last updated timestamp |
providerName |
Institution name |
Document object
| Field | Description |
|---|---|
fileType |
MIME type |
id |
Document id |
name |
Name |
size |
Size in bytes |
Portfolio object
Returned by List portfolios.
| Field | Description |
|---|---|
id |
Portfolio id — use as {portfolioId} |
name |
Portfolio name |
currency |
Portfolio currency |
Asset / Debt / Insurance object
The three share one shape; category tells them apart.
| Field | Description |
|---|---|
id |
Asset / debt / insurance id — use as {itemId} |
name |
Name |
category |
Type — asset / debt / insurance |
subType |
More detailed item type |
description |
Item description |
note |
Item note |
value |
Value object |
cost |
Cost object |
quantity |
Quantity |
rate |
Rate object |
ownership |
Ownership percentage |
irr |
IRR |
cashIn |
Value object |
cashOut |
Value object |
committedCapital |
Value object |
unfunded |
Value object |
investable |
Investable type — non_investable / investable_easy_convert / investable_cash |
ticker |
Ticker symbol |
tickerId |
Ticker id |
tickerSector |
Sector name |
tickerSubType |
Asset class |
isin |
ISIN |
exchange |
Exchange name |
accountNumber |
Account number |
holdingsCount |
Number of holdings |
parent |
Parent account object |
sectionId |
Containing section id |
sectionName |
Section name |
sheetId |
Containing sheet id |
sheetName |
Sheet name |
costBasisForTax |
Value object |
taxability |
taxable / tax-deferred / tax-free |
taxRate |
Tax percentage |
taxOnUnrealizedGain |
Value object |
connection |
Connection object |
Example — holding
A holding belongs to a parent account and carries a parent object.
{
"id": "9f4445b3-0b25-49fb-b1d9-4387e081639d_8E4L9XLl6MudjEpwPAAgivmdZRdBPJuvMPlPb",
"name": "Nflx Feb 01'18 $355 Call",
"sectionId": "9fcbee08-3524-470f-9352-ba99f0f6cf37",
"sectionName": "Chase - Plaid IRA - 5555",
"sheetId": "627a1796-f28e-4646-a796-d983e8576d46",
"sheetName": "Sheet 6",
"category": "asset",
"value": { "amount": 110, "currency": "USD" },
"ticker": "USD",
"tickerId": 150,
"tickerSubType": null,
"tickerSector": "Other",
"quantity": 110,
"irr": 1099900,
"investable": "investable_easy_convert",
"ownership": 100,
"description": null,
"note": null,
"isin": null,
"subType": "derivative",
"holdingsCount": 0,
"cost": { "amount": 0.01, "currency": "USD" },
"costBasisForTax": { "amount": 0.01, "currency": "USD" },
"taxRate": 30,
"taxability": "taxable",
"taxOnUnrealizedGain": { "amount": 32.997, "currency": "USD" },
"connection": {
"aggregator": "plaid",
"providerName": "Chase",
"lastUpdatedTimestamp": 1723452761,
"id": "9DorR9zEmNsqA6xvyM1JtmolBWNWNjfRzBpv4",
"accountId": "P6LzkBlD1Xhn3B649KLvcxE14Jm37Vuo47Wzk"
},
"parent": {
"id": "9f4445b3-0b25-49fb-b1d9-4387e081639d",
"name": "Chase - Plaid IRA - 5555"
}
}
Example — account
A top-level account has no parent and reports holdingsCount > 0.
{
"id": "9f4445b3-0b25-49fb-b1d9-4387e081639d",
"name": "Chase - Plaid IRA - 5555",
"sectionId": "9fcbee08-3524-470f-9352-ba99f0f6cf37",
"sectionName": "Chase - Plaid IRA - 5555",
"sheetId": "627a1796-f28e-4646-a796-d983e8576d46",
"sheetName": "Sheet 6",
"category": "asset",
"value": { "amount": 249.20000000000002, "currency": "USD" },
"ticker": "USD",
"tickerId": 150,
"tickerSubType": null,
"tickerSector": "Other",
"quantity": 249.20000000000002,
"irr": 522.8442889277682,
"investable": "investable_easy_convert",
"ownership": 100,
"description": null,
"note": null,
"isin": null,
"subType": "investment",
"holdingsCount": 2,
"cost": { "amount": 40.01, "currency": "USD" },
"costBasisForTax": { "amount": 40.01, "currency": "USD" },
"taxRate": 30,
"taxability": "taxable",
"taxOnUnrealizedGain": { "amount": 62.757000000000005, "currency": "USD" },
"connection": {
"aggregator": "plaid",
"providerName": "Chase",
"lastUpdatedTimestamp": null,
"id": "9DorR9zEmNsqA6xvyM1JtmolBWNWNjfRzBpv4",
"accountId": "P6LzkBlD1Xhn3B649KLvcxE14Jm37Vuo47Wzk"
}
}
Cash flow object
| Field | Type | Description |
|---|---|---|
cashIn |
number | null | Money in |
cashOut |
number | null | Money out |
currency |
string | Currency code |
date |
string | YYYY-MM-DD |
note |
string | Free-text note |
Ticker object
| Field | Type | Description |
|---|---|---|
id |
number | Kubera's internal ticker id |
ticker |
string | The short symbol. This is the value to send as ticker when creating an item |
name |
string | Full instrument name |
type |
string | stock, fund, derivative, bond, crypto, fiat or index |
subType |
string | Finer classification, e.g. Common Stock |
code |
string | Exchange code for the instrument |
market |
string | Exchange or market, e.g. NASDAQ |
currency |
string | Currency the instrument is priced in |
countryCode |
string | ISO country code |
Sheet object
| Field | Type | Description |
|---|---|---|
id |
string | Sheet id |
name |
string | Sheet name |
category |
string | Asset, Debt or Insurance. Decides which side of net worth the sheet's items count on |
portfolioId |
string | Portfolio the sheet belongs to |
sortKey |
string | Numeric string ordering the sheet within its category |
updateFrequency |
number | Reminder cadence for refreshing manual values. See Create sheet |
tsArchive |
number | Epoch seconds when the sheet was archived; 0 when it is active |
irr |
string | Internal rate of return, when computed |
sections |
array | Section objects. Returned by Create sheet, not by Update sheet |
Section object
| Field | Type | Description |
|---|---|---|
id |
string | Section id |
name |
string | Section name |
sheetId |
string | Sheet the section sits on |
sortKey |
string | Numeric string ordering the section within its sheet |
expanded |
number | 1 if it renders expanded in the app, 0 if collapsed. Cosmetic only |
columnSortKey |
string | Column the section is sorted by in the app |
columnSortOrder |
string | Sort direction for that column |
tsArchive |
number | Epoch seconds when archived; 0 when active |
Fund schedule object
A scheduled capital call or distribution. These do not affect IRR, returns or net worth — see Get fund schedule.
| Field | Type | Description |
|---|---|---|
type |
string | capitalcall or distribution |
date |
string | YYYY-MM-DD. May be in the future |
value |
number | Always positive — direction comes from type |
currency |
string | Currency of value |
note |
string | Free-text note; empty string when unset |
Recap Series object
One entry in results .
{
"timeRange": "weekly",
"report": "asset_classes",
"currency": "USD",
"valueType": "number",
"rows": []
}
| Field | Description |
|---|---|
timeRange |
One of the requested time ranges |
report |
One of the requested report IDs |
currency |
Always present |
valueType |
number or percentage — see report types |
rows |
Array of Recap Row objects |
Recap Row object
| Field | Present on | Description |
|---|---|---|
label |
All rows | Display name of the group or holding |
dataPoints |
All rows | The row’s time series, ordered oldest → newest. Each point is { "date": "YYYY-MM-DD", "value": number } |
children |
Group rows | Nested rows. Omitted on leaf rows — never an empty array |
category |
Most rows | asset or debt |
itemId |
Leaf rows | The asset/debt id — use it with the item endpoints |
sectionId, sheetId |
Where applicable | Ids of the containing section and sheet |
portfolioId |
Leaf rows | Owning portfolio — differs from the requested one for linked portfolios |
isArchived |
Leaf rows | Present and true when the item is archived |
isLinkedPortfolio |
Leaf rows | Present and true when the row comes from a linked portfolio |
To distinguish a group row from a leaf row, test for the presence of children — leaf rows omit the key entirely.
Unofficial SDKs
These are not maintained or verified by the Kubera team. Use at your own risk.