Your own holdings, not a public dataset. These are the only /v1 endpoints that write, and the only ones whose answer differs per API key: a portfolio belongs to the account its key belongs to, and nothing in a path, body or query string can name another account. A portfolio you do not own is reported as 404, exactly like one that does not exist.
The same portfolios are reachable from an AI assistant through the portfolio MCP tools and from your Equibles dashboard. All three read and write the same rows.
How a lot is recorded
A lot is one acquisition: what was bought or sold, how much of it, at what price, and when. Positions are lots rolled up for display, so the original cost and date always stay recoverable.
quantityis signed. Positive is long, negative is a short sale or a written (sold-to-open) option. Shares for a stock, contracts for an option.costPerUnitis a price, so it is never negative: what was paid for a long, what was received for a short. The direction is carried by the sign of the quantity alone. For an option it is the premium per share of the underlying: two contracts at $12.40 isquantity: 2,costPerUnit: 12.40, not 1240.- An option is identified by its OCC symbol (
optionContract). Its expiration, strike, type and contract size are read back from the live options data rather than parsed out of the symbol, so an unknown or expired symbol is rejected instead of stored. Find symbols with the options endpoints. - Quantities and costs are never restated for a stock split. Which side of a split a broker's figures were taken on is unknowable here, and guessing would corrupt your cost basis.
- A sale is a close, not an edit. Closing records the sale price and date and moves the result into
realizedGain; editing the quantity down instead loses it. Closing part of a lot splits it, and the remainder keeps its original cost basis. - All values are US dollars. A portfolio holds at most 200 lots, and an account at most 10 portfolios.
How a portfolio is priced
Each position is marked from its own price lane and names both the lane and the trading session:
markSource: "SettledClose", the official end-of-day close, withmarkAsOfnaming its session.markSource: "LiveQuote", a live reading, used only when it belongs to a newer trading day than the settled close. Once the official row for that day is written, the settled close takes over again.markSource: "OptionDailyClose", the contract's own daily snapshot, attributed to the session the provider dated it with.markAsOf: nullmeans the session is unknown, never today.markSource: null, meaning the position could not be priced.markUnavailableReasonsays why, andmarkPerUnit,marketValueandunrealizedGainare allnull. No price is never reported as a price of zero, which would read as a total loss.
costBasis covers every open lot, priced or not, because what was paid is always known. marketValue and the unrealized figures cover only the priced ones, and unvaluedPositionCount is what tells you the totals cover part of the portfolio rather than all of it. coverageNote is set when a bound stopped a pass from pricing everything.
A portfolio can also watch instruments it does not hold. A watched instrument appears in the valuation's watching array with a mark from the same price lanes and nothing else: no quantity, cost or value fields exist on a watch row, and it is never part of costBasis, marketValue or any gain figure. Watching an instrument you then buy shows one row, not two: the held position appears in positions carrying its watchId, and watching lists only what is not held. A portfolio watches at most 200 instruments.
Percentages are plain numbers: 57.9335 means 57.9335%.
GET /v1/portfolios
Your portfolios and how many lots each holds. Deliberately unpriced: valuing every portfolio to render a list would fan out to the market data for each one.
Parameters: none.
curl "https://api.equibles.com/v1/portfolios" \
-H "Authorization: Bearer eq_your_api_key"import requests
r = requests.get(
"https://api.equibles.com/v1/portfolios",
headers={"Authorization": "Bearer eq_your_api_key"},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios", {
headers: { Authorization: "Bearer eq_your_api_key" },
});
console.log(await res.json());{
"data": [
{
"id": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"name": "Main",
"openLotCount": 2,
"closedLotCount": 1,
"createdAtUtc": "2026-02-11T09:14:22Z"
},
{
"id": "2d5e0c41-8b77-4a19-93cc-1f6e4a2b8d90",
"name": "Retirement",
"openLotCount": 5,
"closedLotCount": 0,
"createdAtUtc": "2026-05-30T18:03:41Z"
}
]
}
POST /v1/portfolios
Creates an empty portfolio and returns 201. The name must be unique among your own and is how the MCP tools address it. A duplicate name, or an eleventh portfolio, returns 400 invalid_parameter naming the reason.
Body: name (required, up to 128 characters).
curl -X POST "https://api.equibles.com/v1/portfolios" \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Main"}'import requests
r = requests.post(
"https://api.equibles.com/v1/portfolios",
headers={"Authorization": "Bearer eq_your_api_key"},
json={"name": "Main"},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios", {
method: "POST",
headers: {
Authorization: "Bearer eq_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Main" }),
});
console.log(await res.json());{
"id": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"name": "Main",
"openLotCount": 0,
"closedLotCount": 0,
"createdAtUtc": "2026-02-11T09:14:22Z"
}
GET /v1/portfolios/
One portfolio, priced. Open lots are rolled up into a position per instrument, each carrying its own lots; instruments watched but not held come in watching with marks only; closed lots are listed separately with their realized result.
Parameters: {id} (path), a portfolio id from the list above.
curl "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77" \
-H "Authorization: Bearer eq_your_api_key"import requests
r = requests.get(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
headers={"Authorization": "Bearer eq_your_api_key"},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77", {
headers: { Authorization: "Bearer eq_your_api_key" },
});
console.log(await res.json());{
"id": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"name": "Main",
"asOfUtc": "2026-08-18T17:42:09Z",
"costBasis": 7220.00,
"marketValue": 11402.80,
"unrealizedGain": 4182.80,
"unrealizedGainPercent": 57.9335,
"realizedGain": 1043.25,
"unvaluedPositionCount": 0,
"coverageNote": null,
"positions": [
{
"instrumentKind": "Stock",
"ticker": "NVDA",
"name": "Nvidia Corp",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"quantity": 40.0,
"costBasis": 4740.00,
"averageCostPerUnit": 118.50,
"markPerUnit": 176.32,
"markSource": "LiveQuote",
"markAsOf": "2026-08-18",
"markUnavailableReason": null,
"marketValue": 7052.80,
"unrealizedGain": 2312.80,
"unrealizedGainPercent": 48.7932,
"expired": false,
"lots": [
{
"id": "a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411",
"instrumentKind": "Stock",
"ticker": "NVDA",
"name": "Nvidia Corp",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"quantity": 40.0,
"costPerUnit": 118.50,
"costBasis": 4740.00,
"acquiredDate": "2025-09-02",
"note": "core position",
"closedDate": null,
"closePricePerUnit": null,
"marketValue": 7052.80,
"unrealizedGain": 2312.80,
"realizedGain": null,
"isOpen": true
}
]
},
{
"instrumentKind": "Option",
"ticker": "NVDA",
"name": "Nvidia Corp",
"occTicker": "O:NVDA270115C00200000",
"expirationDate": "2027-01-15",
"strikePrice": 200.0,
"contractType": "Call",
"sharesPerContract": 100,
"quantity": 2.0,
"costBasis": 2480.00,
"averageCostPerUnit": 12.40,
"markPerUnit": 21.75,
"markSource": "OptionDailyClose",
"markAsOf": "2026-08-17",
"markUnavailableReason": null,
"marketValue": 4350.00,
"unrealizedGain": 1870.00,
"unrealizedGainPercent": 75.4032,
"expired": false,
"lots": [
{
"id": "b7c48e19-2a05-4de3-91f6-3d8a5c1b7e22",
"instrumentKind": "Option",
"ticker": "NVDA",
"name": "Nvidia Corp",
"occTicker": "O:NVDA270115C00200000",
"expirationDate": "2027-01-15",
"strikePrice": 200.0,
"contractType": "Call",
"sharesPerContract": 100,
"quantity": 2.0,
"costPerUnit": 12.40,
"costBasis": 2480.00,
"acquiredDate": "2026-03-17",
"note": null,
"closedDate": null,
"closePricePerUnit": null,
"marketValue": 4350.00,
"unrealizedGain": 1870.00,
"realizedGain": null,
"isOpen": true
}
]
}
],
"watching": [
{
"id": "d4b90f21-7c3a-4e8f-b6a1-92e5c7d80f34",
"instrumentKind": "Stock",
"ticker": "AMD",
"name": "Advanced Micro Devices Inc",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"markPerUnit": 172.94,
"markSource": "SettledClose",
"markAsOf": "2026-08-17",
"markUnavailableReason": null,
"expired": false
},
{
"id": "f8a2c655-1e09-4b7d-a3c8-40d61b9e2a17",
"instrumentKind": "Option",
"ticker": "AMD",
"name": "Advanced Micro Devices Inc",
"occTicker": "O:AMD270115C00200000",
"expirationDate": "2027-01-15",
"strikePrice": 200.0,
"contractType": "Call",
"sharesPerContract": 100,
"markPerUnit": 14.85,
"markSource": "OptionDailyClose",
"markAsOf": "2026-08-17",
"markUnavailableReason": null,
"expired": false
}
],
"closedLots": [
{
"id": "c92b6f37-5e18-4c74-a0d5-71e93b6a4f08",
"instrumentKind": "Stock",
"ticker": "MSFT",
"name": "Microsoft Corp",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"quantity": 15.0,
"costPerUnit": 402.10,
"costBasis": 6031.50,
"acquiredDate": "2025-11-04",
"note": null,
"closedDate": "2026-06-26",
"closePricePerUnit": 471.65,
"marketValue": null,
"unrealizedGain": null,
"realizedGain": 1043.25,
"isOpen": false
}
]
}
POST /v1/portfolios/
Records one purchase or sale and returns 201 with the stored lot.
Retrying is safe. An identical request within a few minutes returns the lot that already landed, with "deduplicated": true and HTTP 200 instead of 201, so a retried timeout never leaves you holding the position twice.
Body: ticker (required; for an option, the underlying), quantity (required, signed), costPerUnit (required, positive), acquiredDate (required, YYYY-MM-DD, not in the future), optionContract (optional OCC symbol), note (optional, up to 256 characters).
curl -X POST "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots" \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"ticker": "NVDA", "optionContract": "O:NVDA270115C00200000", "quantity": 2, "costPerUnit": 12.40, "acquiredDate": "2026-03-17"}'import requests
r = requests.post(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots",
headers={"Authorization": "Bearer eq_your_api_key"},
json={
"ticker": "NVDA",
"optionContract": "O:NVDA270115C00200000",
"quantity": 2,
"costPerUnit": 12.40,
"acquiredDate": "2026-03-17",
},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots", {
method: "POST",
headers: {
Authorization: "Bearer eq_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
ticker: "NVDA",
optionContract: "O:NVDA270115C00200000",
quantity: 2,
costPerUnit: 12.4,
acquiredDate: "2026-03-17",
}),
});
console.log(await res.json());{
"portfolioId": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"portfolioName": "Main",
"deduplicated": false,
"lot": {
"id": "b7c48e19-2a05-4de3-91f6-3d8a5c1b7e22",
"instrumentKind": "Option",
"ticker": "NVDA",
"name": "Nvidia Corp",
"occTicker": "O:NVDA270115C00200000",
"expirationDate": "2027-01-15",
"strikePrice": 200.0,
"contractType": "Call",
"sharesPerContract": 100,
"quantity": 2.0,
"costPerUnit": 12.40,
"costBasis": 2480.00,
"acquiredDate": "2026-03-17",
"note": null,
"closedDate": null,
"closePricePerUnit": null,
"marketValue": null,
"unrealizedGain": null,
"realizedGain": null,
"isOpen": true
}
}
A lot returned by a write is unpriced: marketValue and unrealizedGain are null because a write answers about what was recorded, not what it is worth. Fetch the portfolio to have it valued.
PATCH /v1/portfolios/
Corrects a lot that was entered wrong. Only the fields you send change.
The instrument itself cannot be edited: a lot on the wrong stock or the wrong contract is a different holding, so delete it and add the right one.
Body: any of quantity, costPerUnit, acquiredDate, note. Send "note": "" to clear a note.
curl -X PATCH "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411" \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"costPerUnit": 118.50, "note": "core position"}'import requests
r = requests.patch(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411",
headers={"Authorization": "Bearer eq_your_api_key"},
json={"costPerUnit": 118.50, "note": "core position"},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411", {
method: "PATCH",
headers: {
Authorization: "Bearer eq_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ costPerUnit: 118.5, note: "core position" }),
});
console.log(await res.json());{
"portfolioId": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"portfolioName": "Main",
"deduplicated": false,
"lot": {
"id": "a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411",
"instrumentKind": "Stock",
"ticker": "NVDA",
"name": "Nvidia Corp",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"quantity": 40.0,
"costPerUnit": 118.50,
"costBasis": 4740.00,
"acquiredDate": "2025-09-02",
"note": "core position",
"closedDate": null,
"closePricePerUnit": null,
"marketValue": null,
"unrealizedGain": null,
"realizedGain": null,
"isOpen": true
}
}
POST /v1/portfolios/
Records a sale. Omit quantity to close the whole lot, or pass less to close part of it. A partial close splits the lot, so the returned closed lot carries its own id and the remainder stays open on the original cost basis.
Body: closePricePerUnit (required, positive), closeDate (required, YYYY-MM-DD, not before the lot was acquired), quantity (optional, unsigned).
curl -X POST "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411/close" \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"closePricePerUnit": 471.65, "closeDate": "2026-06-26"}'import requests
r = requests.post(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411/close",
headers={"Authorization": "Bearer eq_your_api_key"},
json={"closePricePerUnit": 471.65, "closeDate": "2026-06-26"},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/a3f1c204-9d6e-4a58-8b12-0f7c2d9e5411/close", {
method: "POST",
headers: {
Authorization: "Bearer eq_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ closePricePerUnit: 471.65, closeDate: "2026-06-26" }),
});
console.log(await res.json());{
"portfolioId": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"portfolioName": "Main",
"deduplicated": false,
"lot": {
"id": "c92b6f37-5e18-4c74-a0d5-71e93b6a4f08",
"instrumentKind": "Stock",
"ticker": "MSFT",
"name": "Microsoft Corp",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"quantity": 15.0,
"costPerUnit": 402.10,
"costBasis": 6031.50,
"acquiredDate": "2025-11-04",
"note": null,
"closedDate": "2026-06-26",
"closePricePerUnit": 471.65,
"marketValue": null,
"unrealizedGain": null,
"realizedGain": 1043.25,
"isOpen": false
}
}
DELETE /v1/portfolios/
Removes a lot as though it had never been recorded, for something entered by mistake. A sale is recorded by closing the lot instead, which keeps its realized profit and loss. Returns 204 with no body, and cannot be undone.
curl -X DELETE "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/b7c48e19-2a05-4de3-91f6-3d8a5c1b7e22" \
-H "Authorization: Bearer eq_your_api_key"import requests
r = requests.delete(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/b7c48e19-2a05-4de3-91f6-3d8a5c1b7e22",
headers={"Authorization": "Bearer eq_your_api_key"},
)
print(r.status_code)const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/lots/b7c48e19-2a05-4de3-91f6-3d8a5c1b7e22", {
method: "DELETE",
headers: { Authorization: "Bearer eq_your_api_key" },
});
console.log(res.status);POST /v1/portfolios/
Puts an instrument on the portfolio's watchlist without recording any position, and returns 201 with the stored watch row. Pass optionContract with the OCC symbol to watch an option, or omit it to watch the stock. The instrument is verified against the live reference data before anything is stored, exactly like a lot's, so an unknown ticker or contract is rejected rather than saved. Watching an instrument the portfolio already watches returns 400 invalid_parameter saying so.
The write answers with identity only; the mark fields fill in on the priced GET, which lists the row in watching.
Body: ticker (required; for an option, the underlying's ticker), optionContract (optional OCC symbol).
curl -X POST "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/watches" \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"ticker": "AMD"}'import requests
r = requests.post(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/watches",
headers={"Authorization": "Bearer eq_your_api_key"},
json={"ticker": "AMD"},
)
print(r.json())const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/watches", {
method: "POST",
headers: {
Authorization: "Bearer eq_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ ticker: "AMD" }),
});
console.log(await res.json());{
"portfolioId": "6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
"portfolioName": "Main",
"watch": {
"id": "d4b90f21-7c3a-4e8f-b6a1-92e5c7d80f34",
"instrumentKind": "Stock",
"ticker": "AMD",
"name": "Advanced Micro Devices Inc",
"occTicker": null,
"expirationDate": null,
"strikePrice": null,
"contractType": null,
"sharesPerContract": null,
"markPerUnit": null,
"markSource": null,
"markAsOf": null,
"markUnavailableReason": null,
"expired": false
}
}
To watch an option instead, name the contract alongside its underlying, the same pair the lot endpoint takes: {"ticker": "AMD", "optionContract": "O:AMD270115C00200000"}. A contract with no published contract size is still watchable — a watch row has no economics for the multiplier to compute — though it could not be held as a lot.
DELETE /v1/portfolios/
Stops watching an instrument. Removes only the watch row — any lots on the same instrument are untouched. Returns 204 with no body. The {watchId} comes from the valuation's watching array, or from the answer to the POST above.
curl -X DELETE "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/watches/d4b90f21-7c3a-4e8f-b6a1-92e5c7d80f34" \
-H "Authorization: Bearer eq_your_api_key"import requests
r = requests.delete(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/watches/d4b90f21-7c3a-4e8f-b6a1-92e5c7d80f34",
headers={"Authorization": "Bearer eq_your_api_key"},
)
print(r.status_code)const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77/watches/d4b90f21-7c3a-4e8f-b6a1-92e5c7d80f34", {
method: "DELETE",
headers: { Authorization: "Bearer eq_your_api_key" },
});
console.log(res.status);DELETE /v1/portfolios/
Deletes a portfolio and every lot in it, including the closed ones and their realized history. Returns 204 with no body, and cannot be undone.
curl -X DELETE "https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77" \
-H "Authorization: Bearer eq_your_api_key"import requests
r = requests.delete(
"https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77",
headers={"Authorization": "Bearer eq_your_api_key"},
)
print(r.status_code)const res = await fetch("https://api.equibles.com/v1/portfolios/6f1d2a80-4f3c-4b6a-9a21-6c0b8f4d1e77", {
method: "DELETE",
headers: { Authorization: "Bearer eq_your_api_key" },
});
console.log(res.status);Privacy
A portfolio is yours. It is stored against your account, it is never used to build market data or any public dataset, and it is never shown to another account. Deleting a portfolio deletes its lots with it.