Skip to main content
This tutorial runs the full Noonum workflow from the REST API. It assumes you’ve done Build a strategy, so you know what an objective, a strategy build, and conviction-ranked holdings are. It adds the parts that guide doesn’t cover: optimizing holdings into a weighted portfolio and backtesting that portfolio against a market benchmark. Each step gives you a curl command and the equivalent Python snippet using the requests library.

What you’ll build

  1. Authenticate and smoke-test the connection.
  2. Browse the public catalog and your own strategies.
  3. Look up companies by ticker.
  4. Create a strategy from an objective, submit it, and poll until it’s ready.
  5. Fetch the strategy’s companies and a constructed portfolio with weights.
  6. Backtest those holdings against a benchmark (e.g. SPY).
The submit-then-poll loop and the optimize retry are the two spots where you wait on async work:

Prerequisites

RequirementValue
REST base URLhttps://api.noonum.ai/v2
Auth headerAuthorization: Bearer <token>
<token>Your Noonum API key
Toolscurl for the shell examples; Python 3.9+ with requests for the code
The <token> is your Noonum API key. See Authentication for how to obtain and send it. Install the Python dependency if you plan to follow the code snippets:
pip install requests
1

Authenticate and smoke-test

Confirm the service is reachable and your token works.Hit the unauthenticated health check to verify connectivity:
curl -X GET "https://api.noonum.ai/v2/health"
A 200 OK with {"status":"OK","message":"Service is running"} means the API is up.Now make your first authenticated call. Listing your strategies validates the token: it returns 200 (even if the list is empty) when the token is good, and 401 Unauthorized when it isn’t. Replace YOUR_API_KEY with your key:
curl -X GET "https://api.noonum.ai/v2/strategies" \
  -H "Authorization: Bearer YOUR_API_KEY"
The equivalent set-up in Python. Every later snippet reuses this BASE_URL and headers:
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.noonum.ai/v2"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

resp = requests.get(f"{BASE_URL}/strategies", headers=headers)
resp.raise_for_status()  # raises on 401/4xx/5xx
print(f"Authenticated. You own {len(resp.json())} strategies.")
One token, two transports The same Bearer token works against the MCP server at https://api.noonum.ai/v1/mcp (HTTP transport). To drive Noonum from an AI agent, you won’t need new credentials. See the MCP overview.
2

Browse the public catalog

Noonum publishes a catalog of public strategies. Inspect them to see the data shape before you build your own. Pass scope=public to list the catalog (public, non-archived, completed strategies):
curl -X GET "https://api.noonum.ai/v2/strategies?scope=public" \
  -H "Authorization: Bearer YOUR_API_KEY"
public = requests.get(
    f"{BASE_URL}/strategies",
    params={"scope": "public"},
    headers=headers,
)
public.raise_for_status()

catalog = public.json()
catalog_id = catalog[0]["id"]  # a public strategy id; reused later
for s in catalog[:5]:
    print(s["id"], "-", s["name"])
Each item is a Strategy object with an id, name, objective, and status. Read the companies inside any public strategy with GET /strategies/{strategyId}/companies, the same endpoint you’ll use for your own strategies in Step 5. Public strategies also expose read-only per-company evidence and historical runs through those same /strategies/{strategyId}/... endpoints, covered in Working with public strategies below.
One scope, three views scope=user (the default) lists your own strategies, scope=public lists the catalog, and scope=all returns the deduplicated union of both.
3

Look up companies by ticker

Most Noonum endpoints identify companies by a UUID, not a ticker. Resolve a ticker, name, ISIN, or FIGI fragment to a company id with the helper search endpoint:
curl -X POST "https://api.noonum.ai/v2/helper/companies/search" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "TSLA"}'
lookup = requests.post(
    f"{BASE_URL}/helper/companies/search",
    json={"query": "TSLA"},
    headers=headers,
)
lookup.raise_for_status()

match = lookup.json()["results"][0]
company_id = match["id"]
print("Resolved:", match["name"], "->", company_id)
Results are ranked with exact matches first, then prefix matches, capped at the top 10. Each result lists its active securities (primary listing first). Any endpoint that takes a company id, including the backtest later in this tutorial, can use an id resolved here.
4

Create and build a strategy

This is the create → submit → poll loop from Build a strategy, in code: create the strategy (synchronous), submit to start the async theme analysis, then poll until it’s done.

4a. Create

POST /strategies requires a name and an objective; exclusions is optional.
curl -X POST "https://api.noonum.ai/v2/strategies" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Electric Vehicles",
    "objective": "Identify companies focused on EV manufacturing, battery technology, and charging-station infrastructure."
  }'
create = requests.post(
    f"{BASE_URL}/strategies",
    json={
        "name": "Electric Vehicles",
        "objective": (
            "Identify companies focused on EV manufacturing, battery technology, "
            "and charging-station infrastructure."
        ),
    },
    headers=headers,
)
create.raise_for_status()

strategy = create.json()
strategy_id = strategy["id"]
print("Created strategy:", strategy_id)
A 201 Created returns the new Strategy object. Save its id. A fresh strategy has active_version_id: null and status: 0 until you submit it.

4b. Submit for processing

Submitting kicks off the async build. With no as_of_date, this is a LIVE run against the current draft:
curl -X POST "https://api.noonum.ai/v2/strategies/STRATEGY_ID/submit" \
  -H "Authorization: Bearer YOUR_API_KEY"
submit = requests.post(f"{BASE_URL}/strategies/{strategy_id}/submit", headers=headers)
submit.raise_for_status()
A 423 Locked here means a run for this version and date is already in flight. Wait for it to finish before resubmitting.

4c. Poll until complete

Poll GET /strategies/{strategyId} and watch status, the percent-complete field; 100 means the build has finished.
from time import sleep

while True:
    poll = requests.get(f"{BASE_URL}/strategies/{strategy_id}", headers=headers)
    poll.raise_for_status()

    status = poll.json()["status"]
    print("status:", status)
    if status == 100:
        break
    if status in (-1, -2):
        raise RuntimeError(f"Run failed with status {status}")
    sleep(10)

print("Strategy build complete.")
The status codes are 0 not started, 195 in progress, 100 done, -1 error, and -2 permanently failed.
Add exclusions to refine results To drop part of a result, add natural-language exclusions with PATCH /strategies/{strategyId}, then resubmit. Each submit of a changed draft becomes a new version. The Iterate a strategy guide covers this loop in depth.
5

Fetch holdings

With the strategy built, retrieve its companies. Pass includeReasoning=true to also get the human-readable explanation for each inclusion.
curl -X GET "https://api.noonum.ai/v2/strategies/STRATEGY_ID/companies?includeReasoning=true" \
  -H "Authorization: Bearer YOUR_API_KEY"
companies = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/companies",
    params={"includeReasoning": "true"},
    headers=headers,
)
companies.raise_for_status()

holdings = companies.json()
print(f"{len(holdings)} companies in the strategy.")
for c in holdings[:5]:
    print(f"  {c['symbol']:6} {c['name']:30} conviction={c.get('convictionScore')}")
By default this reads the active version’s latest completed run. Each entry is a Company with id, symbol, name, sector, marketCap, linguisticBeta, marketBuzz, and a convictionScore in (0, 1). The convictionScore is the default ranking metric, an overall thematic-strength score independent of company size. See Signals and scores for what each field means.

Turn the list into a weighted portfolio

The raw company list is unweighted. To get an investable portfolio with per-holding weights, use the construction endpoint. POST /strategies/{strategyId}/optimize tilts the weights by a signal score (default convictionScore):
curl -X POST "https://api.noonum.ai/v2/strategies/STRATEGY_ID/optimize?signalType=convictionScore&maxWeight=0.2" \
  -H "Authorization: Bearer YOUR_API_KEY"
optimize = requests.post(
    f"{BASE_URL}/strategies/{strategy_id}/optimize",
    params={"signalType": "convictionScore", "maxWeight": 0.2},
    headers=headers,
)
optimize.raise_for_status()

result = optimize.json()
weighted = result["portfolio"]
print("Constructed", result["metadata"]["numberStocks"], "holdings")
for c in weighted[:5]:
    print(f"  {c['symbol']:6} weight={c['weight']:.4f}")
The response carries a metadata block (number of holdings, weighted signal score, and expected return/volatility/Sharpe stats) plus a portfolio array where every company also has a weight. A 404 Not Found here means there is no completed run yet for the resolved version and date. Keep polling Step 4c until status == 100, then retry.
6

Backtest the holdings

Validate the portfolio against history. POST /backtest takes a list of holdings (each a companyId + weight), resolves each to a tradeable security, and computes a buy-and-hold NAV with risk statistics versus a benchmark.Build the request from the constructed portfolio you fetched:
backtest_holdings = [
    {"companyId": c["id"], "weight": c["weight"]} for c in weighted
]

backtest = requests.post(
    f"{BASE_URL}/backtest",
    json={
        "holdings": backtest_holdings,
        "years": 5,
        "benchmark": "SPY",
    },
    headers=headers,
)
backtest.raise_for_status()

stats = backtest.json()["metadata"]
bench = backtest.json()["benchmarkMetadata"]
print(f"Portfolio CAGR: {stats['annualizedReturn']:.2%}  Sharpe: {stats['sharpeRatio']}")
print(f"{bench['symbol']} CAGR:     {bench['annualizedReturn']:.2%}  Sharpe: {bench['sharpeRatio']}")
The same call with curl (using two holdings for brevity):
curl -X POST "https://api.noonum.ai/v2/backtest" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "holdings": [
      {"companyId": "83d88f60-f581-4176-bb5c-e0b463d1d442", "weight": 0.5},
      {"companyId": "0c8b9628-6fce-4d95-a787-5b532172a6ea", "weight": 0.5}
    ],
    "years": 5,
    "benchmark": "SPY"
  }'
The response includes:
  • metadata: total returns, annualizedReturn (CAGR), variance, sharpeRatio, maxDrawdown, plus stockCount, weekCount, and any warnings.
  • timeSeries: [timestamp, navValue] pairs rebased to 10,000.
  • benchmarkMetadata and benchmarkTimeSeries: the same stats for the benchmark, aligned to the portfolio’s dates so you can chart them together.
years accepts 1–20 (default 5) and benchmark accepts any symbol such as SPY or QQQ (default SPY). A 400 Bad Request means there was insufficient price data or no companies could be resolved.

Handling errors

Every endpoint shares the same auth and error conventions:
StatusMeaningWhat to do
401 UnauthorizedMissing/invalid token, or an inactive accountRecheck the Authorization header; revisit Authentication
400 Bad RequestMalformed body or invalid identifier (e.g. a non-UUID company id), or a future as_of_dateRead the error field in the JSON body and fix the input
404 Not FoundThe strategy, company, or run doesn’t exist or isn’t visible to you, or there is no completed run for the selected version/dateVerify the id, or poll until a run completes
423 LockedA run for this version and date is already in flightWait, then resubmit
429 Too Many RequestsThe historical submission rate limit was exceededBack off; the body reports available/requested credits
Error responses use a consistent shape, so you can surface the message directly:
resp = requests.get(f"{BASE_URL}/strategies/{strategy_id}", headers=headers)
if not resp.ok:
    detail = resp.json().get("error", resp.text)
    raise RuntimeError(f"Noonum API {resp.status_code}: {detail}")

Working with public strategies

The public catalog (GET /strategies?scope=public) exposes the same read-only surface as your own strategies. Beyond listing them and fetching their companies (Step 2), you can inspect the evidence behind each holding and pull historical runs. Every endpoint below takes a public strategy id: the catalog_id you captured in Step 2, not the strategy_id of the strategy you built.

Get evidence for a specific company

This endpoint explains why a company is in the strategy: a human-readable reasoning plus supporting summaries (evidence excerpts). It takes the catalog_id and a company id from that strategy’s holdings:
holdings_resp = requests.get(
    f"{BASE_URL}/strategies/{catalog_id}/companies",
    headers=headers,
)
holdings_resp.raise_for_status()
catalog_company_id = holdings_resp.json()[0]["id"]

evidence_resp = requests.get(
    f"{BASE_URL}/strategies/{catalog_id}/companies/{catalog_company_id}",
    headers=headers,
)
evidence_resp.raise_for_status()

company_evidence = evidence_resp.json()
print("isIncluded:", company_evidence.get("isIncluded"))
print("reasoning:", company_evidence.get("reasoning"))
print("summaries (count):", len(company_evidence.get("summaries", [])))

for s in company_evidence.get("summaries", [])[:3]:
    print(f"  [{s['provider']}] {s['text'][:120]}...")
Each summary includes provider (the source), pubDate (a Unix timestamp), and text (the evidence content).

Get evidence for every company at once

Fetch reasoning and evidence for all included companies in the strategy in a single call:
all_evidences_resp = requests.get(
    f"{BASE_URL}/strategies/{catalog_id}/evidences",
    headers=headers,
)
all_evidences_resp.raise_for_status()

all_evidences = all_evidences_resp.json()["evidences"]
print("Companies with evidences:", len(all_evidences))

List the dates with historical data

Strategy runs are dated. Check which as-of dates have a completed run before requesting one:
dates_resp = requests.get(
    f"{BASE_URL}/strategies/{catalog_id}/available-dates",
    headers=headers,
)
dates_resp.raise_for_status()

available_dates = dates_resp.json()["available_dates"]
print("Available dates:", available_dates)
The response is {"available_dates": ["2024-01-31", "2024-02-29", ...]} in ascending order. Only dates with a completed run appear.

Download the holdings for a historical date

Read the company list as it stood on a specific date with the same companies endpoint, passing asOfDate:
date = available_dates[0]
historical_resp = requests.get(
    f"{BASE_URL}/strategies/{catalog_id}/companies",
    params={"asOfDate": date},
    headers=headers,
)
historical_resp.raise_for_status()

companies = historical_resp.json()
print(f"Companies for {date}:", len(companies))
The shape matches the live results. An asOfDate with no completed run returns 404 Not Found; pick another date from available_dates.
On your own strategies, you generate historical results by submitting a run with a past as_of_date. See Part 2 of the iteration walkthrough for the submit-poll-read loop.

Next steps

You now have the full loop: authenticate → build → fetch holdings → optimize → backtest. From here:
  • Refine results with exclusions in the Iterate a strategy guide.
  • Browse every endpoint, with request/response schemas and an interactive try-it console, in the API Reference.
  • Drive Noonum from an AI agent with the same token via the MCP overview.