# Indexes API

> SentiSense's composite sentiment indexes in one standardized envelope: a single scalar per index, its history, and the constituent breakdown behind it.

Base URL: `https://app.sentisense.ai/api/v1/indexes`
Tier: Free (API key required)

All API access requires an API key via the `X-SentiSense-API-Key` header. Keys look like `ss_live_...`.

> SDKs: [Python](https://github.com/SentiSenseApp/sentisense) · [Node](https://github.com/SentiSenseApp/sentisense-node)

## Overview

An index is a single number that means something, tracked over time. SentiSense publishes several, each blending its own inputs into one scalar on a stated scale, and all of them answer on the same envelope: one value, an `asOf` date, the scale it lives on, and where available the breakdown that produced it. Write one renderer and you get every current and future SentiSense index.

**Currently live indexes:**

| Index id | Scale | accessTier | What it measures |
|---|---|---|---|
| `market-mood` | `PERCENT_0_100` | free | Proprietary fear-and-greed composite blending social sentiment, market direction, risk appetite, social momentum, S&P 500 trend, and options flow |
| `fed-sentiment` | `SENTIMENT` | free | Weekly weighted sentiment across Federal Reserve leadership and the institution itself, with role-anchored weights that survive a Chair transition |
| `ai-sentiment` | `SENTIMENT` | free | Daily sentiment across AI-exposed names, spanning compute and infrastructure through the AI-native companies |

The discovery endpoint is the source of truth for what is live. More indexes ship over time.

**Two archetypes, one envelope.** Most indexes here are *baskets*: a weighted average across a curated set of tracked entities. For those, `constituents`, `basketSize`, `coverage`, and `totalMentions` describe exactly how the headline was built, so you can audit it or discount a low-coverage day. Market Mood is a *composite* instead, built from six market and social signals rather than a set of entities, so those four fields are `null` on it. That `null` is the answer, not a gap: it means the index has no constituents by construction. Branch on it rather than treating it as missing data.

**Access:** send your API key on every call. Each index declares an `accessTier` of `"free"` or `"pro"` in the discovery listing. Every index is `"free"` today, which means no response on this page is tier-branched: FREE and PRO callers receive identical payloads. Requests still count against your monthly quota and your per-minute rate limit. Read `accessTier` from the listing rather than assuming, so a future `pro` index does not surprise your client.

**Scales:** `SENTIMENT` is signed polarity in `[-1, +1]`, where negative is bearish tone and positive is bullish. `PERCENT_0_100` is a 0-100 composite where higher is greedier. Read the `scale` field and set your axis bounds and color bands from it; do not infer them from the index id.

---

## GET /api/v1/indexes

Discovery endpoint. Returns every index the platform publishes, with the metadata you need to build a hub, a navigation menu, or a typed client.

**Parameters:** none.

**Example response:**

```json
{
  "indexes": [
    {
      "indexId": "fed-sentiment",
      "displayName": "Fed Sentiment",
      "description": "Weekly weighted composite sentiment across Fed leadership and the Federal Reserve institution.",
      "scale": "SENTIMENT",
      "accessTier": "free",
      "canonicalUrl": "/api/v1/indexes/fed-sentiment"
    },
    {
      "indexId": "market-mood",
      "displayName": "Market Mood",
      "description": "SentiSense proprietary 0-100 composite blending social sentiment, market direction, volatility, and momentum.",
      "scale": "PERCENT_0_100",
      "accessTier": "free",
      "canonicalUrl": "/api/v2/market-mood"
    }
  ]
}
```

**Response fields:**

| Field | Type | Description |
|-------|------|-------------|
| `indexId` | string | URL slug. Use it as the `{indexId}` path parameter below |
| `displayName` | string | Human-readable name, suitable for a card title |
| `description` | string | One sentence; suitable for a card subtitle |
| `scale` | string | `"SENTIMENT"` (signed, `[-1, +1]`) or `"PERCENT_0_100"` |
| `accessTier` | string | `"free"` or `"pro"`. Every index is `"free"` today |
| `canonicalUrl` | string | Where the richest view of this index lives. See the note below |

**On `canonicalUrl`:** for most indexes this is simply `/api/v1/indexes/{indexId}`, the detail endpoint documented next. Market Mood is the exception: it points at [`/api/v2/market-mood`](/docs/api/market-mood/), which carries a phase label, a weekly change, the six-signal breakdown, and an 11-sector map that the shared index envelope has no room for.

That is a pointer to a *richer* view, not a redirect around a missing one. Every id this endpoint advertises also resolves at `/api/v1/indexes/{indexId}` and `/api/v1/indexes/{indexId}/history`, so a generic client can iterate the listing without special-casing anything. Follow `canonicalUrl` when you want everything an index has; use the detail endpoint when you want every index to answer the same shape.

---

## GET /api/v1/indexes/{indexId}

Latest reading for one index. For basket indexes the constituent breakdown is recomputed per request, so you can see which entities contributed and how fresh each reading was.

**Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `indexId` | string | Yes | Index slug from the discovery listing (path parameter) |

**Example response** (a basket index):

```json
{
  "indexId": "fed-sentiment",
  "displayName": "Fed Sentiment",
  "asOf": "2026-06-01",
  "value": 0.12,
  "scale": "SENTIMENT",
  "coverage": 3,
  "basketSize": 3,
  "totalMentions": 480,
  "methodologyNote": "Weekly composite of per-entity mean sentiment over each Monday-Sunday bucket...",
  "constituents": [
    {
      "kbEntityId": "kb/person/618",
      "displayName": "Example Person",
      "role": "Chair",
      "weight": 3.0,
      "value": 0.15,
      "mentionsCount": 260,
      "staleness": "FRESH",
      "contribution": 0.098,
      "link": "https://sentisense.ai/entities/example-person"
    }
  ]
}
```

**Response fields:**

| Field | Type | Description |
|-------|------|-------------|
| `indexId` | string | Index slug |
| `displayName` | string | Human-readable name |
| `asOf` | string | Date the reading covers, `YYYY-MM-DD`. For weekly indexes this is the start of the bucket |
| `value` | number | The headline scalar, on `scale` |
| `scale` | string | `"SENTIMENT"` or `"PERCENT_0_100"` |
| `coverage` | integer | Constituents that actually contributed. `null` on composite indexes |
| `basketSize` | integer | Constituents in the basket on this date. `null` on composite indexes |
| `totalMentions` | integer | Mentions behind the reading. `null` on composite indexes |
| `methodologyNote` | string | How the value was computed, and what any `null` fields mean |
| `constituents` | array | Per-entity breakdown. `null` on composite indexes |

**Constituent fields:**

| Field | Type | Description |
|-------|------|-------------|
| `kbEntityId` | string | Ontology entity id. Resolvable via the [Entities API](/docs/api/entities/) |
| `displayName` | string | Entity name |
| `role` | string | The entity's role in this basket, which is what carries the weight |
| `weight` | number | Relative weight on this date |
| `value` | number | The entity's own reading |
| `mentionsCount` | integer | Mentions behind that reading in the lookback window |
| `staleness` | string | `FRESH`, `CARRIED_FORWARD`, `EXCLUDED`, or `OUT_OF_SEGMENT`. See below |
| `contribution` | number | Reserved. **Currently returns `null` on every constituent**; see the note below before using it |
| `link` | string | Detail page for the entity, or `null` when there is no resolvable target |

**`contribution` is not populated yet.** It is reserved for this entity's share of the headline
value, and it serves `null` on every constituent today. Do not build on it. You can derive the
same number client-side from fields that are populated: for a weighted-average index, a
constituent's share of the headline is `weight * value` divided by the sum of `weight` across the
constituents whose `staleness` is not `EXCLUDED`.

**Reading `staleness`:** `FRESH` means the entity was mentioned inside the lookback window. `CARRIED_FORWARD` means it was not, so its last known reading is standing in. `EXCLUDED` means it is in the basket but has no usable reading at all and was renormalized out. `OUT_OF_SEGMENT` means it is not in the basket on this date, which is how the API reports an entity before it took the role that gives it weight; its `weight` will be `0` and the row is present only for transparency. A day where most constituents are `CARRIED_FORWARD` is a thin day, and `coverage` is the fast way to spot one.

**Weights are anchored to roles, not to people.** When the occupant of a weighted role changes, the weight moves with the role on the transition date. That keeps the series continuous instead of putting a step change in the chart on the day of a handover.

---

## GET /api/v1/indexes/{indexId}/history

Historical scalar series for one index, for charting. Returns the stored series, so this is a read rather than a recomputation.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `indexId` | string | Yes | | Index slug from the discovery listing (path parameter) |
| `days` | int | No | 180 | Days of history to return |

**Example response:**

```json
{
  "indexId": "fed-sentiment",
  "displayName": "Fed Sentiment",
  "scale": "SENTIMENT",
  "days": 180,
  "history": [
    { "date": "2026-05-18", "value": 0.08 },
    { "date": "2026-05-25", "value": 0.12 }
  ]
}
```

**Response fields:**

| Field | Type | Description |
|-------|------|-------------|
| `indexId` | string | Index slug |
| `displayName` | string | Human-readable name |
| `scale` | string | `"SENTIMENT"` or `"PERCENT_0_100"` |
| `days` | integer | The window you requested, echoed back |
| `history` | array | Points of `{ date, value }`, oldest first |

**Point spacing follows the index, not the calendar.** A weekly index returns one point per Monday-Sunday bucket; a daily index returns one point per day. Market Mood returns trading days only, since the market half of the composite is frozen over a weekend while the social half drifts, which would otherwise put a weekend wobble in the line. Plot against the `date` field rather than assuming a fixed interval.

**Thin buckets are withheld.** An index may suppress an early-life or low-coverage bucket rather than publish a number it cannot defend, so `history` can be shorter than the requested window and can contain gaps. Treat missing dates as absent, not as zero.
