Skip to main content

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 null if you are on the last page
  • previous: The URL for the previous page, or null if 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

To fetch all items efficiently, start with 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
}
  1. Verify your API key starts with twai_ (or pk_ for older keys)
  2. Ensure the Authorization header uses the Bearer scheme
  3. 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
}
  1. Confirm the key has the required scopes (portfolio:read for read access, portfolio:write for write access)
  2. 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
}
  1. Read the Retry-After header and wait the specified number of seconds
  2. Implement exponential backoff for retry logic
  3. Consider caching responses to reduce the number of requests

Best Practice

Implement automatic retries with exponential backoff: wait 1 second after the first 429 response, 2 seconds after the second, 4 seconds after the third, and so on.

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

We use cookies to improve your experience. See our Cookie Policy (opens in a new tab) for details.