> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noonum.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# End-to-end REST integration

> A working end-to-end Noonum REST integration. Authenticate, build a strategy, fetch holdings, and run a backtest.

This tutorial runs the full Noonum workflow from the REST API. It assumes you've done
[Build a strategy](/v2/guides/build-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`](https://requests.readthedocs.io/) 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:

```mermaid theme={null}
flowchart TD
    A["1 · Authenticate<br/>GET /health, GET /strategies"] --> B{"Token valid?"}
    B -->|"401"| C["Re-check Authorization header"]
    C --> A
    B -->|"200"| P["2 · Browse the public catalog<br/>GET /strategies?scope=public"]
    P --> D["3 · Look up companies<br/>POST /helper/companies/search"]
    D --> E["4 · Create and submit<br/>POST /strategies<br/>POST /strategies/{strategyId}/submit"]
    E --> F{"Poll GET /strategies/{strategyId}<br/>status hits 100?"}
    F -->|"not yet"| F
    F -->|"yes"| G["5 · Fetch holdings, optimize<br/>GET /strategies/{strategyId}/companies<br/>POST /strategies/{strategyId}/optimize"]
    G -->|"404 no completed run"| F
    G -->|"200"| H["6 · Backtest<br/>POST /backtest"]
    H --> I["Review vs benchmark<br/>CAGR, Sharpe ratio, max drawdown"]
```

## Prerequisites

| Requirement   | Value                                                                   |
| ------------- | ----------------------------------------------------------------------- |
| REST base URL | `https://api.noonum.ai/v2`                                              |
| Auth header   | `Authorization: Bearer <token>`                                         |
| `<token>`     | Your Noonum API key                                                     |
| Tools         | `curl` for the shell examples; Python 3.9+ with `requests` for the code |

The `<token>` is your **Noonum API key**. See
[Authentication](/v2/getting-started/authentication) for how to obtain and send it.

Install the Python dependency if you plan to follow the code snippets:

```bash theme={null}
pip install requests
```

<Steps>
  <Step title="Authenticate and smoke-test">
    Confirm the service is reachable and your token works.

    Hit the unauthenticated health check to verify connectivity:

    ```bash theme={null}
    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:

    ```bash theme={null}
    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`:

    ```python theme={null}
    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.")
    ```

    <Tip>
      **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](/base/agentic/overview).
    </Tip>
  </Step>

  <Step title="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):

    ```bash theme={null}
    curl -X GET "https://api.noonum.ai/v2/strategies?scope=public" \
      -H "Authorization: Bearer YOUR_API_KEY"
    ```

    ```python theme={null}
    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](#working-with-public-strategies) below.

    <Tip>
      **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.
    </Tip>
  </Step>

  <Step title="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:

    ```bash theme={null}
    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"}'
    ```

    ```python theme={null}
    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.
  </Step>

  <Step title="Create and build a strategy">
    This is the create → submit → poll loop from
    [Build a strategy](/v2/guides/build-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.

    ```bash theme={null}
    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."
      }'
    ```

    ```python theme={null}
    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:

    ```bash theme={null}
    curl -X POST "https://api.noonum.ai/v2/strategies/STRATEGY_ID/submit" \
      -H "Authorization: Bearer YOUR_API_KEY"
    ```

    ```python theme={null}
    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.

    ```python theme={null}
    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, `1`–`95` in progress, `100` done, `-1` error, and
    `-2` permanently failed.

    <Tip>
      **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](/v2/guides/iterate-strategy) guide covers this loop in
      depth.
    </Tip>
  </Step>

  <Step title="Fetch holdings">
    With the strategy built, retrieve its companies. Pass `includeReasoning=true` to also get
    the human-readable explanation for each inclusion.

    ```bash theme={null}
    curl -X GET "https://api.noonum.ai/v2/strategies/STRATEGY_ID/companies?includeReasoning=true" \
      -H "Authorization: Bearer YOUR_API_KEY"
    ```

    ```python theme={null}
    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](/base/concepts/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`):

    ```bash theme={null}
    curl -X POST "https://api.noonum.ai/v2/strategies/STRATEGY_ID/optimize?signalType=convictionScore&maxWeight=0.2" \
      -H "Authorization: Bearer YOUR_API_KEY"
    ```

    ```python theme={null}
    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.
  </Step>

  <Step title="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:

    ```python theme={null}
    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):

    ```bash theme={null}
    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.
  </Step>
</Steps>

## Handling errors

Every endpoint shares the same auth and error conventions:

| Status                  | Meaning                                                                                                                         | What to do                                                                                       |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `401 Unauthorized`      | Missing/invalid token, or an inactive account                                                                                   | Recheck the `Authorization` header; revisit [Authentication](/v2/getting-started/authentication) |
| `400 Bad Request`       | Malformed body or invalid identifier (e.g. a non-UUID company id), or a future `as_of_date`                                     | Read the `error` field in the JSON body and fix the input                                        |
| `404 Not Found`         | The strategy, company, or run doesn't exist or isn't visible to you, or there is no completed run for the selected version/date | Verify the id, or poll until a run completes                                                     |
| `423 Locked`            | A run for this version and date is already in flight                                                                            | Wait, then resubmit                                                                              |
| `429 Too Many Requests` | The historical submission rate limit was exceeded                                                                               | Back off; the body reports `available`/`requested` credits                                       |

Error responses use a consistent shape, so you can surface the message directly:

```python theme={null}
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:

```python theme={null}
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:

```python theme={null}
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:

```python theme={null}
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`:

```python theme={null}
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`.

<Note>
  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](/v2/guides/iterate-strategy) for the
  submit-poll-read loop.
</Note>

## 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](/v2/guides/iterate-strategy) guide.
* Browse every endpoint, with request/response schemas and an interactive try-it console,
  in the [API Reference](/v2/api-reference/introduction).
* Drive Noonum from an AI agent with the same token via the
  [MCP overview](/base/agentic/overview).
