API Code Examples
Ready-to-use code snippets for integrating with the Portfolio Tracker Consumer API using cURL, Python, and JavaScript.
Last reviewed
cURL Examples
Quick command-line examples for testing the API directly.
GET Holdings
Retrieve a list of all holdings in your portfolio. Replace twai_your_api_key with your actual API key.
curl -X GET "https://orchard72.com/api/v1/portfolio/ext/holdings/" \ -H "Authorization: Bearer twai_your_api_key" \ -H "Content-Type: application/json"
POST Transaction
Record a new buy or sell transaction against a holding.
curl -X POST "https://orchard72.com/api/v1/portfolio/ext/transactions/" \
-H "Authorization: Bearer twai_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"security": "3f1a9c22-8f4e-4b0a-9f21-0b6d5b2e77aa",
"account": "b7d0e1f4-2a53-4c8b-9c3d-51e0a4f9c112",
"transaction_type": "buy",
"quantity": 10,
"price_per_unit": "152.35",
"currency": "USD",
"transaction_date": "2026-04-01T14:30:00Z"
}'GET Analytics
Analytics are exposed as two endpoints, allocation and performance. There is no combined summary endpoint on the REST API; the MCP tool get_analytics_summary is the one that merges both.
curl -X GET "https://orchard72.com/api/v1/portfolio/ext/analytics/allocation/" \ -H "Authorization: Bearer twai_your_api_key" \ -H "Content-Type: application/json" curl -X GET "https://orchard72.com/api/v1/portfolio/ext/analytics/performance/" \ -H "Authorization: Bearer twai_your_api_key" \ -H "Content-Type: application/json"
Python Examples
Integration examples using the popular requests library.
Authentication Setup
Start by creating a reusable session with your API key. All subsequent requests will include the correct authorisation header automatically.
import requests
API_KEY = "twai_your_api_key"
BASE_URL = "https://orchard72.com/api/v1/portfolio/ext"
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
})List Holdings
Fetch all holdings and print each security’s name and current total value. The holdings endpoint returns every row in one response under a holdings key, alongside a summary object. It is not paginated.
response = session.get(f"{BASE_URL}/holdings/")
response.raise_for_status()
data = response.json()
for holding in data["holdings"]:
print(f"{holding['company_name']}: {holding['total_value']} {holding['currency']}")
print(data["summary"])Create a Transaction
Record a new buy transaction and handle the response.
payload = {
"security": "3f1a9c22-8f4e-4b0a-9f21-0b6d5b2e77aa",
"account": "b7d0e1f4-2a53-4c8b-9c3d-51e0a4f9c112",
"transaction_type": "buy",
"quantity": 10,
"price_per_unit": "152.35",
"currency": "USD",
"transaction_date": "2026-04-01T14:30:00Z",
}
response = session.post(f"{BASE_URL}/transactions/", json=payload)
if response.status_code == 201:
transaction = response.json()
print(f"Transaction {transaction['id']} created successfully")
else:
print(f"Error {response.status_code}: {response.text}")Pagination Handling
List endpoints such as transactions return paginated results. Use the page query parameter to iterate through all pages. The holdings endpoint is the exception and returns everything at once.
def fetch_all_transactions(session, base_url):
"""Fetch every transaction across all pages."""
transactions = []
page = 1
while True:
response = session.get(
f"{base_url}/transactions/",
params={"page": page},
)
response.raise_for_status()
data = response.json()
transactions.extend(data["results"])
if not data["next"]:
break
page += 1
return transactions
all_transactions = fetch_all_transactions(session, BASE_URL)
print(f"Fetched {len(all_transactions)} transactions in total")JavaScript Examples
Browser and Node.js examples using the Fetch API.
Authentication Setup
Create a helper function that includes the authorisation header with every request.
const API_KEY = "twai_your_api_key";
const BASE_URL = "https://orchard72.com/api/v1/portfolio/ext";
async function apiRequest(path, options = {}) {
const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new Error(
`API error ${response.status}: ${body?.error || response.statusText}`
);
}
return response.json();
}List Holdings
Retrieve your holdings and log each one to the console.
const data = await apiRequest("/holdings/");
for (const holding of data.holdings) {
console.log(`${holding.company_name}: ${holding.total_value} ${holding.currency}`);
}Create a Transaction
Submit a new transaction and handle the result.
const transaction = await apiRequest("/transactions/", {
method: "POST",
body: JSON.stringify({
security: "3f1a9c22-8f4e-4b0a-9f21-0b6d5b2e77aa",
account: "b7d0e1f4-2a53-4c8b-9c3d-51e0a4f9c112",
transaction_type: "buy",
quantity: 10,
price_per_unit: "152.35",
currency: "USD",
transaction_date: "2026-04-01T14:30:00Z",
}),
});
console.log(`Transaction ${transaction.id} created successfully`);Error Handling
Wrap API calls in a try/catch block and handle common error scenarios gracefully.
try {
const data = await apiRequest("/holdings/");
console.log(`Loaded ${data.holdings.length} holdings`);
} catch (error) {
if (error.message.includes("401")) {
console.error("Authentication failed. Check your API key.");
} else if (error.message.includes("429")) {
console.error("Rate limit exceeded. Please wait before retrying.");
} else {
console.error("Unexpected error:", error.message);
}
}Pagination
How paginated responses work and how to iterate through all pages.
Page Number Pattern
List endpoints return paginated results using a page query parameter. The page size is 100 items. The holdings endpoint is not paginated and returns every holding in one response.
- page: The page of results to return, starting at 1
- count: The total number of items available across all pages
- next: The URL for the next page of results, or
nullif you are on the last page - previous: The URL for the previous page, or
nullif you are on the first page
Example Response
A typical paginated response looks like the following. Use the next field to determine whether more pages are available.
{
"count": 124,
"next": "https://orchard72.com/api/v1/portfolio/ext/transactions/?page=2",
"previous": null,
"results": [
{
"id": "9c2f7b41-6d18-42a7-8a55-1d3c7e9f0b02",
"security_symbol": "AAPL",
"security_name": "Apple Inc.",
"transaction_type": "buy",
"transaction_type_display": "Buy",
"transaction_date": "2026-04-01T14:30:00Z",
"quantity": "15.0000000000",
"price_per_unit": "152.3500000000",
"total_amount": "2285.25",
"currency": "USD"
}
]
}Tip
page=1 and increment it until next is null.Iterating Through Pages (JavaScript)
async function fetchAllTransactions() {
const allTransactions = [];
let page = 1;
while (true) {
const data = await apiRequest(`/transactions/?page=${page}`);
allTransactions.push(...data.results);
if (!data.next) break;
page += 1;
}
return allTransactions;
}Error Handling
Common error responses and how to handle them in your application.
Common Error Responses
The API uses standard HTTP status codes. Below are the most common errors you may encounter and guidance on handling each one.
401 Unauthorised
Returned when the API key is missing, invalid, or has been revoked.
{
"error": "Authentication credentials were not provided.",
"status_code": 401
}- Verify your API key starts with
twai_(orpk_for older keys) - Ensure the
Authorizationheader uses theBearerscheme - Check that the key has not been revoked in your developer settings
403 Forbidden
Returned when your API key does not have permission to access the requested resource or action.
{
"error": "You do not have permission to perform this action.",
"status_code": 403
}- Confirm the key has the required scopes (
portfolio:readfor read access,portfolio:writefor write access) - Check that the resource belongs to the account associated with this key
429 Too Many Requests
Returned when you exceed the rate limit for your plan. The response includes a Retry-After header indicating how many seconds to wait before making another request.
{
"error": "Request was throttled. Expected available in 30 seconds.",
"message": "Request was throttled. Expected available in 30 seconds.",
"status_code": 429,
"error_code": "RATE_LIMITED",
"wait_time": 30
}- Read the
Retry-Afterheader and wait the specified number of seconds - Implement exponential backoff for retry logic
- Consider caching responses to reduce the number of requests
Best Practice
Retry Pattern (Python)
A simple retry helper that respects the Retry-After header and backs off gracefully.
import time
def api_request_with_retry(session, url, max_retries=3, **kwargs):
"""Make an API request with automatic retry on rate limiting."""
for attempt in range(max_retries):
response = session.get(url, **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
raise Exception("Maximum retries exceeded")Related Topics
Can’t find what you’re looking for?
Our support team is here to help. Contact us and we’ll get back to you as soon as possible.
Contact Support
