# Stocks API

> Stock prices delayed 15 minutes, historical charts, company fundamentals, short interest data, and AI-powered stock analysis.

Base URL: `https://app.sentisense.ai/api/v1/stocks`
Tier: Free (deep AI summaries PRO-gated)

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#stocks) · [Node](https://github.com/SentiSenseApp/sentisense-node#stocks) · example: [basic-usage.ts](https://github.com/SentiSenseApp/sentisense-node/blob/main/examples/basic-usage.ts)

## Overview

The Stocks API provides comprehensive stock data including 15-minute-delayed prices, historical charts, company fundamentals, short interest metrics, and AI-powered analysis reports. Every endpoint requires an API key (free to generate in **Settings > Developer Console**); most are available in full on the Free tier.

---

## Stock Lists & Metadata

### GET /

Returns all available stock ticker symbols tracked by SentiSense.

**Authentication:** API key required | **Parameters:** None

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks"
```

**Response:** `string[]` (e.g., `["AAPL", "MSFT", "GOOGL", ...]`)

---

### GET /detailed

Returns all stocks with company name, KB entity ID, and URL slug.

**Authentication:** API key required | **Parameters:** None

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/detailed"
```

**Response object:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Ticker symbol |
| `name` | string | Company name |
| `kbEntityId` | string | Ontology entity ID |
| `urlSlug` | string | URL-friendly slug |
| `socialDominance` | object \| null | Precomputed share of voice. `value` is share as a 0-1 decimal, `rank` is 1-based across the coverage universe, `percentile` is 0-100. Null when no signal exists. Refreshed daily. |

---

### GET /popular

Returns popular stock tickers. **Parameters:** None

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/popular"
```

**Response:** `string[]`

---

### GET /popular/detailed

Returns popular stocks with company details. Same schema as `/detailed`.

---

### GET /images

Returns company logo/icon URLs for a batch of tickers.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `tickers` | string | Yes | - | Comma-separated tickers (max 600) |
| `forced` | boolean | No | false | Bypass cache |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/images?tickers=AAPL,TSLA,NVDA"
```

**Response:** `Map<ticker, { iconUrl, logoUrl }>`: URLs for company icons and logos. `GET` a URL directly to receive the image bytes; no API key is required for the image fetch itself, so the URLs can be used straight in an `<img src>` tag.

Treat these URLs as refreshable rather than permanent. Brand assets are periodically refreshed, and a URL issued for a previous revision stops resolving once that happens. Re-read them from this endpoint instead of storing them long term.

---

### GET /descriptions

Returns company profiles with branding, market cap, and sector information.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `tickers` | string | Yes | - | Comma-separated tickers |
| `forced` | boolean | No | false | Bypass cache |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/descriptions?tickers=AAPL,MSFT"
```

---

## Per-Stock Data

### GET /{ticker}/similar

Returns peer and similar stocks based on sector, industry, and SentiSense ontology relationships.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker symbol |
| `limit` | integer | No | 5 | Max results |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/similar?limit=10"
```

---

### GET /{ticker}/profile

Returns a company profile including CEO, sector, industry, and market data.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker symbol |
| `forced` | boolean | No | false | Bypass cache |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/profile"
```

**Listing status:** the profile also carries `listingStatus`, `delistedDate` and `delistingReason` when the symbol is delisted or pending delisting. All three are absent for a normally listed symbol, and they carry the same values as on [GET /price](#get-price): `listingStatus` is `"DELISTED"` or `"PENDING_DELISTING"`, `delistedDate` is the ISO date trading stopped, and `delistingReason` is one of `acquired`, `take_private`, `bankruptcy`, `exchange_rule`, `merged`.

**ETF profiles:** when `ticker` identifies a tracked ETF, the response may also carry `imageUrl`, a square presentation image for the fund. It is the issuer's mark rather than the individual fund's, so every fund in a family shares one image, and it is the same value returned by the `/etfs` endpoints. It is square like `logoUrl` and `iconUrl` and drops into the same avatar slot, but it is a first-party asset rather than a vendor branding mark, so it is returned as a direct URL. It is absent for issuers we hold no image for.

---

### GET /{ticker}/entities

Returns SentiSense ontology entities related to a stock (e.g., CEO, key products, partner companies).

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/entities"
```

**Response:** array of related entities. Each entry:

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Deprecated: not a stable public identifier. Use `urlSlug` to refer to an entity. |
| `displayName` | string | Entity display name |
| `type` | string | `PERSON`, `PRODUCT`, `ORGANIZATION`, etc. |
| `relatedStock` | string | The ticker you queried |
| `urlSlug` | string\|null | Handle for the [Metrics API](/docs/api/metrics/) `{entityId}` parameter (e.g. `Tim-Cook`) |
| `title` | string\|null | Person's role (e.g. `CEO`) |
| `category` | string\|null | Product category |
| `appId` | string\|null | Apple App Store id when the product has a tracked companion app |

---

### GET /{ticker}/graph

Returns the typed relationships around one stock, including leaders, products and product families, peers, owners and subsidiaries, publishers, and topics.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/graph?depth=1&cap=75"
```

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `depth` | integer | No | 1 | Traversal depth, either `1` or `2` |
| `cap` | integer | No | 75 | Maximum non-root nodes, from `1` to `200` |

**Response:** a bounded graph projection with the requested stock as its root.

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Uppercase ticker symbol |
| `root` | string | Slug of the root company; matches one entry in `nodes` |
| `depth` | integer | Applied traversal depth |
| `cap` | integer | Applied non-root node cap |
| `truncated` | boolean | `true` when more matching nodes exist beyond `cap` |
| `omitted` | integer | Nodes left out because they carry no slug, and so cannot be addressed |
| `counts` | object | Returned `nodes`, `edges`, and node totals in `byType` |
| `groups` | object | Slugs grouped as `people`, `products`, `productFamilies`, `peers`, `organizations`, `publishers`, and `topics` |
| `nodes[]` | array | Nodes with `slug`, `displayName`, and `type`; `PRODUCT_OR_SERVICE` nodes also carry a string `category` when non-null, matching the product category on `/{ticker}/entities` |
| `edges[]` | array | Relationships with `source`, `target`, `type`, `direction`, and `properties` |

```json
{
  "ticker": "AAPL",
  "root": "Apple-Inc",
  "depth": 1,
  "cap": 75,
  "truncated": false,
  "omitted": 0,
  "counts": {
    "nodes": 3,
    "edges": 2,
    "byType": { "COMPANY": 1, "PERSON": 1, "PRODUCT_OR_SERVICE": 1 }
  },
  "groups": {
    "people": ["Tim-Cook"],
    "products": ["iPhone"],
    "productFamilies": [],
    "peers": [],
    "organizations": [],
    "publishers": [],
    "topics": []
  },
  "nodes": [
    { "slug": "Apple-Inc", "displayName": "Apple Inc.", "type": "COMPANY" },
    { "slug": "Tim-Cook", "displayName": "Tim Cook", "type": "PERSON" },
    { "slug": "iPhone", "displayName": "iPhone", "type": "PRODUCT_OR_SERVICE", "category": "Smartphones" }
  ],
  "edges": [
    { "source": "Tim-Cook", "target": "Apple-Inc", "type": "LEADS", "direction": "DIRECTED", "properties": {} },
    { "source": "iPhone", "target": "Apple-Inc", "type": "PRODUCT_OF", "direction": "DIRECTED", "properties": { "sourceProperty": "associatedCompany" } }
  ]
}
```

**Handles:** slugs are the only identifier here. The same string does three jobs: it joins `groups` and `edges` to `nodes` inside one response, it addresses the entity on the [Metrics API](/docs/api/metrics/) and [Documents API](/docs/api/documents/), and it is the entity page path (`/entities/Tim-Cook`). A product family is `{ "family": "iPhone", "members": ["iPhone-15"] }`, slugs on both sides.

**Edge types:** `OWNS`, `PEER`, `BELONGS_TO`, `LEADS`, `FOUNDED`, `AFFILIATED_WITH`, `VARIANT_OF`, `SUBTOPIC_OF`, and `SUBSIDIARY_OF` are curated relationships. `PRODUCT_OF` is derived from a product's associated company.

**Direction:** `DIRECTED` preserves the relationship from `source` to `target`. `BIDIRECTIONAL` combines matching relationships present in both directions.

**Edge properties:** `properties` maps string keys to string values and is `{}` when the relationship carries no detail. Keys include `role` (the person's title on a `LEADS` edge, e.g. `"Chief Executive Officer"`), `since` (the year a `LEADS` relationship began, e.g. `"2011"`), and `year` (the founding year on a `FOUNDED` edge). Other keys may appear, so treat the map as open.

**Unknown ticker:** returns `404` with `error: "entity_not_found"` and up to three `suggestions` from entity search, plus `reason: "non_us_listing"` when the symbol is a non-US listing.

Free with an API key; rate limits apply. One ticker per call; there is no enumeration endpoint.

---

### GET /{ticker}/ai-summary

Returns an AI-generated stock analysis report. PRO feature with limited Free access: `depth=basic` returns a preheader summary (Free: unlimited). `depth=deep` returns a full multi-section report (Free: 10 views/month, PRO: unlimited). Returns 429 with `{error: "quota_exceeded", message, ...}` when Free quota is exhausted, matching the platform-wide quota contract. `message` says when the allowance resets (the start of next month) and links PRO, which has no monthly report limit. The response has no `Retry-After` header.

**Authentication:** API key required

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker symbol |
| `depth` | string | No | `basic` | Analysis depth: `basic` or `deep` |

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/ai-summary?depth=deep"
```

**Response Schema:** flat object (no `{isPreview, data}` wrapper).

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Ticker symbol |
| `companyName` | string | Company name |
| `status` | string | `READY`, `DRAFT`, `NOT_AVAILABLE`, or `ERROR`. Only `READY` carries report content; treat any other value as no report. Without content, `sections`, `sectionOrder`, `moatRating` and `aiDisruptionRisk` are omitted from the response, and `companyName` and `version` can be omitted too |
| `statusReason` | string \| null | Present on every status other than `READY` |
| `reportType` | string | `SUMMARY` for `depth=basic`, `FULL` for `depth=deep` |
| `version` | integer | Report date encoded as yymmdd (e.g. `260520`) |
| `lastUpdated` | long | Epoch milliseconds |
| `sections` | object | Map of section name to `{content, directives}`. Present on both depths when `status` is `READY`. `depth=basic` returns a single `Executive Summary` section; `depth=deep` returns the full set. |
| `sectionOrder` | string[] | Ordered section keys for rendering. Present on both depths when `status` is `READY`. `["Executive Summary"]` on `depth=basic`. |
| `moatRating` | integer \| null | Proprietary moat quality score 0 to 10. Present on `depth=deep` only. `null` if not yet assessed for this ticker. |
| `aiDisruptionRisk` | string \| null | `Low`, `Medium`, `High`, or `Critical`: AI revenue-displacement exposure. Present on `depth=deep` only. `null` if not yet assessed. |

Check `status == "READY"` before reading any section. **Do not branch on the presence of `sections`** to decide whether you received a deep report: both depths return it. Read `reportType` (`SUMMARY` vs `FULL`), or check the length of `sectionOrder`.

---

### GET /{ticker}/metrics/{metricType}/breakdown

Returns sentiment or mention metrics broken down by entity for a stock over a time range.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker |
| `metricType` | path | Yes | - | Metric type (e.g., `sentiment`, `mentions`) |
| `startTime` | long | Yes | - | Start time (epoch ms) |
| `endTime` | long | Yes | - | End time (epoch ms) |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/metrics/sentiment/breakdown?startTime=1745600000000&endTime=1746204800000"
```

---

## Market Data & Prices

### GET /price

Returns the latest price for a single stock ticker, delayed 15 minutes.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker symbol |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/price?ticker=AAPL"
```

**Response:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Ticker symbol |
| `currentPrice` | double | Regular-session price, delayed 15 minutes. During RTH (09:30 to 16:00 ET): the most recent regular-session value. Otherwise: most recent regular-session close. |
| `change` | double | `currentPrice` change vs `previousClose` |
| `changePercent` | double | `currentPrice` change percentage |
| `previousClose` | double | Previous closing price |
| `volume` | long | Volume |
| `timestamp` | long | When this response was served (epoch milliseconds). Not the age of the price: it tracks the current clock regardless of how old the value is. |
| `priceAsOf` | long \| absent | When the market data behind `currentPrice` is from (epoch milliseconds). This is the field to read for freshness. Omitted outside regular hours, and whenever the upstream data is not dated, so treat its absence as "unknown" rather than "fresh". |
| `extendedHours` | object \| null | Extended-hours view (pre-market or after-hours). Absent during RTH, overnight, and weekends. See below. |
| `listingStatus` | string \| absent | `"DELISTED"` or `"PENDING_DELISTING"`. Present only when the symbol is delisted or pending delisting, absent otherwise. |
| `delistedDate` | string \| absent | ISO date (YYYY-MM-DD) trading stopped. Present only alongside `listingStatus: "DELISTED"`. |
| `delistingReason` | string \| absent | One of `acquired`, `take_private`, `bankruptcy`, `exchange_rule`, `merged`. Present only alongside `listingStatus: "DELISTED"`. |

**Delisted symbols return their last trade price rather than an error, and `listingStatus` is what marks that price as frozen.** On a `"DELISTED"` symbol, `currentPrice`, `change` and `changePercent` are the final trading session's values and never advance again. `priceAsOf` is the close of that final session (it can fall a day or more before `delistedDate`), `extendedHours` is absent, and `timestamp` is still the serve time. Read `listingStatus` before rendering a price as current.

**`extendedHours` object:**

| Field | Type | Description |
|-------|------|-------------|
| `session` | string | `"pre"` (04:00 to 09:30 ET) or `"post"` (16:00 to 20:00 ET) |
| `price` | double | Live extended-hours price |
| `change` | double | Extended-hours price change vs `currentPrice` |
| `changePercent` | double | Extended-hours change percentage vs `currentPrice` |

---

### GET /prices

Returns the latest prices for multiple tickers in a single request, delayed 15 minutes (see `/price` above).

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `tickers` | string | Yes | - | Comma-separated tickers |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/prices?tickers=AAPL,TSLA,NVDA"
```

**Response:** `StockPrice[]` (JSON array). Each element is the same price object as `/price`, including a `ticker` field identifying the symbol, an optional `extendedHours` object during pre/post sessions, and the optional `listingStatus` / `delistedDate` / `delistingReason` fields. Check each element for a frozen price rather than assuming a batch is uniformly live. Tickers that fail to resolve are silently omitted from the array.

---

### GET /chart

Returns historical OHLCV (Open, High, Low, Close, Volume) chart data for a stock.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker symbol |
| `timeframe` | string | No | `1M` | Chart timeframe: `1D`, `5D`, `1W`, `1M`, `3M`, `6M`, `1Y`, `5Y`, `10Y`, `MAX` |

**Up to 26 years of split- and dividend-adjusted history.** `MAX` returns a stock's full available
history, back to 1999 for names listed that long: 320 monthly bars for AAPL, versus the 5 years
most price APIs cap at. `10Y` returns weekly bars over ten years.

Granularity is chosen per range so a long chart stays a sensible payload: intraday for `1D`
through `1M` (5-minute for `1D`, 15-minute for `5D`, 30-minute for `1W`, hourly for `1M`), daily
for `3M` through `1Y`, weekly for `5Y` and `10Y`, monthly for `MAX`.

Ranges of `10Y` and `MAX` are **adjusted for both splits and dividends**, so a long series is
comparable end to end rather than showing a cliff at every corporate action. Shorter ranges,
including `5Y`, are split-adjusted only. The two bases therefore differ on the same historical
date, by roughly the dividends paid since: negligible for a non-payer, around 14% over five years
for a 3% yielder. `volume` is restated to today's share count in every range, so a pre-split bar
reports shares on the current basis and the volume series has no cliff either. Only splits move
it: a dividend changes the price basis but not the number of shares that changed hands. Every
bar carries `adjusted: true` to mark that basis.

One consequence of the restatement surprises people, so it is worth stating plainly: on a stock
with a large cumulative split factor, an old bar can report a share count many times larger than a
recent one. AAPL has split 28-for-1 since 2013, so one share of 2008 is 28 shares now and its raw
turnover is counted 28 times over. Measured on the monthly series in September 2026, that puts a
2008 bar near 24 billion shares against roughly 1 billion for a recent month. That is the
restatement working, not a data error, and the series is continuous across every split.

**`202 Accepted`** on `10Y` and `MAX` means that stock's deep history is still being assembled.
The body is an empty array and a `Retry-After` header gives the suggested wait in seconds; retry
and you will get the full series. We return this rather than silently substituting a shorter
range, so a `200` always means the range you asked for. It only happens on the first request for
a rarely-viewed stock.

A `timeframe` value outside the list above answers `400 Bad Request` with an `invalid_timeframe`
error naming the valid values.

**Response:** array of bars. Each bar has:

| Field | Type | Description |
|-------|------|-------------|
| `timestamp` | long | Unix timestamp in milliseconds |
| `date` | string | Pre-formatted display string (format varies by timeframe) |
| `open` | double | Opening price |
| `high` | double | Highest price during the bar |
| `low` | double | Lowest price during the bar |
| `close` | double | Closing price |
| `volume` | long | Bar volume, on the same restated basis as the prices (see `adjusted`) |
| `session` | string \| null | US-equity session: `pre` (04:00 to 09:30 ET), `regular` (09:30 to 16:00 ET), or `post` (16:00 to 20:00 ET). Populated for intraday timeframes (`1D`, `5D`, `1W`, `1M`); `null` for daily, weekly, and monthly bars (`3M` and longer) that span whole sessions. The `1M` timeframe returns regular-session bars only. |
| `adjusted` | boolean | Always `true`: the bar is restated to the stock's current share count rather than served as it printed at the time. |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/chart?ticker=AAPL&timeframe=6M"
```

Migrating from an internal chart endpoint on another site? `GET /chart` is the documented
equivalent, with full OHLCV bars instead of close-only pairs. Field-by-field mapping:
[the migration guide](/blog/stockanalysis-api-symbol-history-endpoint/).

---

### GET /market-status

Returns the current US market status: `open` while the regular trading session is open, `closed` otherwise. Pre-market and after-hours report as `closed`.

**Parameters:** None

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/market-status"
```

**Response:**

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | `open` or `closed` |
| `timestamp` | long | When the status was computed (epoch milliseconds) |

---

## Fundamentals

Two endpoints share one period object. `/fundamentals` returns a single fiscal period, the
latest by default, plus trailing-twelve-month, price-ratio and market fields where available.
`/fundamentals/history` returns up to 40 quarters or 20 years of the same object with the
statement fields and derived ratios filled in and the rest null. `/fundamentals/periods` lists which periods exist, and
`/fundamentals/current` is a compact price-and-TTM profile. The period object is documented once,
under [GET /fundamentals/history](#get-fundamentals-history).

> **Reporting currency.** Statement figures are served as the filer reports them, in the filer's
> own currency, and are never converted to US dollars. US filers report in USD; foreign filers
> listed as ADRs report in their home currency (ASML in EUR, Toyota in JPY, SK hynix in KRW).
> `/fundamentals`, `/fundamentals/current` and every `/fundamentals/history` period carry an
> optional `reportedCurrency` field ("USD", "EUR", "KRW", ...) naming that currency. When the field
> is **absent, the currency is unknown**, not implicitly USD.
>
> Do not mix these figures with the share price. The listed price is the USD ADR price, so when
> `reportedCurrency` is present and not USD the API serves `peRatio`, `psRatio` and `pbRatio` as
> `null` rather than a cross-currency number. Margins, returns and liquidity ratios have the same currency on both
> sides and are served for every filer.

### GET /fundamentals

Returns one fiscal period. With no `fiscalPeriod` and `fiscalYear`, that is the most recent filed
period for the timeframe, enriched with live market data. With both, it is that period as filed,
without the market data (see the table below). Supplying only one of the two does not select a
period; you get the latest one.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker |
| `timeframe` | string | No | `quarterly` | `quarterly` or `annual` |
| `fiscalPeriod` | string | No | - | Specific period (e.g. `Q4` or `FY`). Only honoured together with `fiscalYear`. |
| `fiscalYear` | string | No | - | Specific fiscal year (e.g. `2024`). Only honoured together with `fiscalPeriod`. |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/fundamentals?ticker=AAPL&timeframe=quarterly"
```

**Response:** one period object, the shape documented under
[GET /fundamentals/history](#get-fundamentals-history), plus the fields that history leaves null:

| Field | On `/fundamentals` |
|---|---|
| `revenueTTM`, `netIncomeTTM`, `epsTTM` | The sum of the filed periods that end with this period and together cover twelve months: four rows for a quarterly filer, two for a filer that reports half-years. `epsTTM` is on the diluted basis where the provider reports one. Null on `timeframe=annual`, null for any line that one of those periods lacks, and null when the filed periods cannot be assembled into a twelve-month window. Across a stock split, `epsTTM` can be built from split-adjusted quarterly figures, so it will not always equal the sum of the history rows' `epsDiluted`. |
| `quartersIncluded` | The number of filed periods in the trailing-twelve-month window: `4` for a quarterly filer, `2` for a half-year filer. The trailing sums are filled only when this is present. Null on `timeframe=annual`, and null when no twelve-month window could be assembled. |
| `currentPrice` | A delayed USD price, cached here for up to 30 minutes; use the quote endpoints when you need the price itself. Latest-period requests only. |
| `marketCap` | USD market capitalisation from the company profile. Latest-period requests only, and null when the profile carries no market cap or the period was served from the statement source (see below). |
| `sharesOutstanding` | Deprecated. On a latest period that carries `marketCap`, it is an estimate `marketCap / currentPrice` rather than a reported count; see the note under `/fundamentals/history`. |
| `peRatio` | `currentPrice / epsTTM`; on `timeframe=annual`, `currentPrice / epsDiluted` for the year (or `epsBasic` when there is no diluted figure). Latest-period requests only, null for non-USD filers and when the EPS input is zero or negative. |
| `psRatio` | `marketCap / revenueTTM`; on `timeframe=annual`, price over the year's revenue per share. Null for non-USD filers and on responses without market data. |
| `pbRatio` | `currentPrice` over book value per share (`totalEquity / sharesOutstanding`). Null for non-USD filers, when equity is not positive, and on responses without market data. |
| `roe`, `roa` | `netIncomeTTM / totalEquity` and `netIncomeTTM / totalAssets`, in percent. On `timeframe=annual`, the year's `netIncome` is used instead. |
| `companyName` | Filled only on latest periods that carry market data. |

The latest period is served from one of two sources, chosen per ticker by which one has the
fresher filing, so a ticker can move between them. Responses from the market-data source carry
the full set above. Responses from the statement source carry the trailing sums, `currentPrice`
and `peRatio` but no `marketCap`, `psRatio`, `pbRatio` or `companyName`, and their
`sharesOutstanding` is the weighted-average form used on history rows. A null `marketCap` on a
latest period tells you which one you got.

---

### GET /fundamentals/periods

Lists the fiscal periods available for a ticker, quarterly and annual together:
`{ ticker, periods: [{ fiscalPeriod, fiscalYear, filingDate, periodEndDate, timeframe }] }`. Order
is not guaranteed; sort on `periodEndDate` yourself.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/fundamentals/periods?ticker=AAPL"
```

---

### GET /fundamentals/current

Returns a compact trailing profile, not a statement: `ticker`, `currentPrice`, `epsTTM`, `revenueTTM`,
`peTTM`, `psTTM`, `quartersIncluded`, `reportedCurrency`, `fetchedAt`, plus `available` and
`reason`. Use it when you only need trailing P/E and P/S; for statement lines use `/fundamentals`.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/fundamentals/current?ticker=AAPL"
```

---

### GET /fundamentals/history

Returns a multi-period history of full financial statements (income statement, balance sheet and
cash flow), one entry per statement period the provider publishes, plus any pre-IPO quarter
described under [Reading the rows](#reading-the-rows), newest first. This is the
endpoint behind the statement tables on the Financials tab: use it for margin trends, multi-year
comparisons, or as the input data for a valuation model. Not the same as `/fundamentals` (a single
period) or `/fundamentals/historical/revenue` (income-statement lines only, recent periods only).

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker |
| `timeframe` | string | No | `quarterly` | `quarterly` or `annual` |
| `limit` | integer | No | 12 quarterly / 10 annual | Periods to return. Values above 40 quarterly / 20 annual are clamped to the cap. |

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/fundamentals/history?ticker=AAPL&timeframe=annual&limit=10"
```

**Response:** `{ ticker, timeframe, count, dataSource, reason, periods[] }`. `count` is the number
of periods returned, below `limit` when the provider has fewer. Periods are sorted by
`periodEndDate`, newest first. A ticker with no statements (an unknown symbol, an ETF or fund, a
very recent listing) returns `200` with `count: 0`, an empty `periods` and a fixed explanatory
`reason`; it is not a `404`. `dataSource` is deprecated and carries no information: `""` on every
period and on a non-empty envelope, `null` on an empty one.

#### The period object

Every field below is present on every period, except `reportedCurrency`, which is omitted when
unknown. `ticker`, `fetchedAt` and `dataSource` are never null. Every other field is `null` when the value is unavailable for that period: the provider did
not publish the line, a consistency check rejected it, or a calculation lacked an input. Null is
never zero, and never a reason to fill in a neighbouring field; the one substitution we recommend
is spelled out in the `sharesOutstanding` note below. On `/fundamentals/history` the last group is
null on every row; those fields are filled only by `/fundamentals`.

**Identity**

| Field | Type | Description |
|---|---|---|
| `ticker` | string | The requested symbol. |
| `fiscalPeriod` | string | `Q1` to `Q4` on quarterly rows, `FY` on annual rows, on the filer's fiscal calendar (Apple's `Q1` ends in December). |
| `fiscalYear` | string | The filer's own fiscal-year label. It can differ from the calendar year of `periodEndDate`: Lululemon's year ending 2025-02-02 is fiscal `"2024"`. |
| `periodEndDate` | string | Last day of the period as an ISO date (`2025-09-27`). The field to sort and join on. |
| `periodMonths` | integer | Months the period covers: `3` for a quarter, `6` for a filer that reports half-years, `12` for a year. `fiscalPeriod` names the quarter a period ends in, so a half-year period is labelled `Q2` or `Q4`; this field is what distinguishes the two. Null when the span cannot be established: the oldest row of a response, which has nothing behind it to measure against, and a row after a missing period whose own figures do not settle its length. |
| `filingDate` | string | Date the statement was filed, ISO date. |
| `companyName` | string | Null on history rows. |

**Income statement** (amounts in `reportedCurrency`)

| Field | Type | Description |
|---|---|---|
| `revenue` | number | Total revenue. |
| `costOfRevenue` | number | Cost of revenue. Served null, together with `grossProfit` and `grossMargin`, when the provider's line fails our consistency check: operating expenses folded into cost of revenue or the reverse, a cost of revenue below zero, or a cost of revenue larger than the period's total operating costs while its gross margin falls far below the neighbouring periods'. |
| `grossProfit` | number | Gross profit. |
| `operatingIncome` | number | Operating income. Null for a period in which the filer's own filings carry no operating-income line (common for banks, insurers and some industrial and pharmaceutical filers, which report income before taxes directly). When the filer's filings cannot be checked, the provider's figure is served. |
| `netIncome` | number | The provider's consolidated net income line. Not the EPS numerator. |
| `bottomLineNetIncome` | number | The provider's bottom-line income line, which can differ from `netIncome` in size and in sign; see the worked example below. Always null on rows from our fallback source. |
| `eps` | number | Compatibility field with a mixed basis: basic EPS, except on fallback-source history rows where it is the diluted figure when one exists. Prefer `epsBasic` or `epsDiluted`, which name their basis. |
| `epsBasic` | number | Basic earnings per share, as reported. |
| `epsDiluted` | number | Diluted earnings per share, as reported. Null when the provider reports no diluted figure. |

**Share counts** (a number of shares)

| Field | Type | Description |
|---|---|---|
| `weightedAverageSharesBasic` | integer | Basic weighted-average shares for the period. Null when the provider reports none. |
| `weightedAverageSharesDiluted` | integer | Diluted weighted-average shares for the period. Null when the provider reports none. |
| `sharesOutstanding` | integer | **Deprecated**, see the note below. On history rows it equals `weightedAverageSharesDiluted`, or the basic figure when there is no diluted one. Always null on rows from our fallback source. |

**Balance sheet** (amounts in `reportedCurrency`)

| Field | Type | Description |
|---|---|---|
| `totalAssets` | number | Total assets. |
| `totalLiabilities` | number | Total liabilities. |
| `totalEquity` | number | Total shareholders' equity. |
| `currentAssets` | number | Current assets. Null for a period in which the filer's balance sheet reports no current-assets total, as with the unclassified balance sheets of banks, insurers and most REITs. When the filer's filings cannot be checked, the provider's figure is served. |
| `currentLiabilities` | number | Current liabilities. Null for a period in which the filer's balance sheet reports no current-liabilities total, on the same terms as `currentAssets`. |
| `cash` | number | Cash and cash equivalents. Does not include short-term investments. |
| `debt` | number | Total debt as the provider reports it. On rows from our fallback source: long-term debt, or non-current liabilities when that line is absent, or total minus current liabilities. |
| `longTermDebt` | number | Long-term debt. |

**Cash flow** (amounts in `reportedCurrency`)

| Field | Type | Description |
|---|---|---|
| `operatingCashFlow` | number | Cash from operations. |
| `investingCashFlow` | number | Cash from investing. |
| `financingCashFlow` | number | Cash from financing. |
| `capitalExpenditure` | number | Capital expenditure, negative as an outflow. A positive provider value is treated as corrupt and served null, along with `freeCashFlow`. Always null on rows from our fallback source. |
| `freeCashFlow` | number | `operatingCashFlow - abs(capitalExpenditure)` when both exist, otherwise the provider's own reported free cash flow, otherwise null. Always null on rows from our fallback source. |

**Derived ratios** (computed by us from the fields above; null when an input is null)

| Field | Type | Description |
|---|---|---|
| `grossMargin` | number | `grossProfit / revenue`, in percent. |
| `operatingMargin` | number | `operatingIncome / revenue`, in percent. |
| `netMargin` | number | `netIncome / revenue`, in percent. |
| `currentRatio` | number | `currentAssets / currentLiabilities`. |
| `debtToEquity` | number | `debt / totalEquity`. |
| `roe` | number | `netIncome / totalEquity` in percent on annual rows. Null on quarterly history rows: it needs a twelve-month figure, which only `/fundamentals` carries. |
| `roa` | number | `netIncome / totalAssets` in percent, same availability as `roe`. |

**Metadata**

| Field | Type | Description |
|---|---|---|
| `reportedCurrency` | string | Currency of the statement amounts ("USD", "EUR", "KRW", ...). Absent when unknown. |
| `fetchedAt` | integer | When we fetched the row from the provider, epoch seconds. |
| `dataSource` | string | Deprecated. Always `""`. |
| `preIpoFiling` | object | The SEC filing a pre-IPO quarter was read from; see Pre-IPO quarters under [Reading the rows](#reading-the-rows). Null on every other row. |

**Always null on history, filled only by `/fundamentals`**

| Fields | Group |
|---|---|
| `revenueTTM`, `netIncomeTTM`, `epsTTM`, `quartersIncluded` | Trailing twelve months |
| `peRatio`, `pbRatio`, `psRatio` | Price ratios (USD price over home-currency figures, so USD filers only) |
| `marketCap`, `currentPrice` | USD market data |

#### Reading the rows

- **Two sources.** History rows come from our primary statement source, which fills every field
  above when the filing carries it. For tickers it does not cover, a fallback source fills the rows
  instead, and its rows are marked in the tables: `bottomLineNetIncome`, `sharesOutstanding`,
  `capitalExpenditure` and `freeCashFlow` are null on every row, `debt` is selected as described,
  and `eps` is the diluted figure where one exists. The row does not say which source served it.
- **Corrections from the filing.** When the provider's figure for a period contradicts the
  company's own SEC filing for that period, and the filing settles which is right, the filing's
  figure is served instead:
  - `revenue`, `costOfRevenue`, `grossProfit` and `operatingIncome`, when the filer's own lines
    for the period are consistent with each other (gross profit plus cost of revenue equals
    revenue, and gross profit less operating expenses equals operating income) and disagree with
    the provider's. The derived margins follow.
  - `weightedAverageSharesBasic`, `weightedAverageSharesDiluted` and the EPS fields of a quarter,
    when the provider carried an earlier period's share counts forward into it and divided by them.
    On a second or third fiscal quarter the corrected counts can be the filing's year-to-date
    averages; the EPS is the quarter's.
  - The EPS fields and share counts of a fiscal year, when the provider's EPS for the year is
    explained by neither its own share count nor the filing's.
  - The EPS fields and both weighted-average share counts of a first, second or third fiscal
    quarter or a fiscal year from roughly the last three (quarters) or five (years) years, when the
    company's SEC filing states all four, in US dollars, for that exact period and they agree with each other and
    with the period's net income. Fourth quarters keep the provider's figures, because an annual
    report states no three-month EPS. A new filing's figures apply once SEC publishes them, usually
    within days; until then the provider's stand.

  A short reviewed list covers periods whose correction the provider's data cannot settle on its
  own; each entry is checked against the filing and stops applying when the provider changes the
  period. Corrected rows are not marked. The trailing sums on `/fundamentals` add the corrected
  figures.
- **Rows are the provider's, not deduplicated.** A provider that republishes a period yields two
  rows with the same `periodEndDate`, and a row whose source date could not be parsed carries null
  there and sorts last. Both are rare; tolerate both.
- **Pre-IPO quarters.** For a small list of recently listed companies, quarterly history includes
  a quarter from before the listing that the company filed only as the prior-year comparison column
  of a later filing, usually its first 10-Q. That row carries `preIpoFiling` naming the filing, and
  holds the income-statement lines the filing reports for the quarter. Its balance-sheet and
  cash-flow fields are null, because such a filing carries no quarter-end balance sheet or
  three-month cash flow for it. When the provider publishes a row for the same period, that row is
  served instead. `preIpoFiling` is null on every other row, including every annual row.

  | Field | Type | Description |
  |---|---|---|
  | `preIpoFiling.form` | string | SEC form type of the filing, for example `10-Q`. |
  | `preIpoFiling.accession` | string | SEC accession number of the filing, `0000000000-00-000000`. |
  | `preIpoFiling.filedDate` | string | Date the filing was filed, `YYYY-MM-DD`. |
  | `preIpoFiling.note` | string | Where in the filing the figures appear. |
- **Share basis.** Per-period EPS is served as the provider reports it, apart from the corrections
  from the filing above. SentiSense restates it onto a split-adjusted basis for a small list of
  named issuers and nowhere else.

  A provider that restates a company's share counts for a split can leave some older rows' EPS on
  the pre-split basis, so those rows contradict themselves. Where that has been checked against the
  company's own filing, SentiSense divides the EPS on the affected rows by the split ratio and
  marks each one with `epsBasisRepair`. Share counts and net income are never changed, and no row
  of any other issuer is changed.

  `epsBasisRepair` is null on almost every row. Null means SentiSense applied no EPS repair to that
  row. It is not a statement that the row's EPS and share count are on the same basis: a row whose
  issuer is not on the list, or whose figures did not qualify, or for which the split history could
  not be read, carries a null marker and is served exactly as the provider reports its EPS.

  | Field | Type | Description |
  |---|---|---|
  | `epsBasisRepair.fields` | string[] | Which EPS fields on the row were restated, in the order `epsBasic`, `epsDiluted`, `eps`. |
  | `epsBasisRepair.multiplier` | number | Restated value divided by the value the provider reported. Divide by it to recover the provider's figure. |
  | `epsBasisRepair.splitExecutionDates` | string[] | Execution dates of the splits the provider had not applied to this row's EPS, newest first, `YYYY-MM-DD`. |

  The Cooper Companies (COO) is the worked example. Its share counts are restated for the
  four-for-one split of 2024-02-20 back to fiscal 2006 while the provider's EPS is restated only
  back to fiscal 2021, so the annual rows through fiscal 2020 carry `epsBasisRepair` with a
  `multiplier` of 0.25: fiscal 2020 basic EPS is served as 1.215 rather than 4.86, against the
  196.4M share count beside it. Fiscal 2021 and fiscal 2022 both carry a null marker. The 7.6x
  drop between them is a real earnings move, not a change of basis.

  `epsBasisRepair` describes per-period EPS only. This repair never changes `epsTTM` or the other
  trailing-twelve-month figures, and the marker never describes a trailing adjustment. Trailing
  figures are assembled separately from the per-period rows, and whether they carry a split
  adjustment of their own depends on which source served them, so the marker says nothing about a
  trailing window that spans one of these seams.

#### Earnings and share-count fields

`epsBasic`, `epsDiluted`, `netIncome`, `bottomLineNetIncome`, `weightedAverageSharesBasic` and
`weightedAverageSharesDiluted` are each the provider's own line, or the filing's where the
corrections described under [Reading the rows](#reading-the-rows) apply. None is computed from another,
**so they are not expected to reconcile arithmetically**: `netIncome / weightedAverageSharesDiluted`
will not generally reproduce any EPS field, because the provider computes EPS from its own numerator
on its own share basis.

`netIncome` and the EPS fields are consolidated figures that include discontinued operations, while
`revenue` and `operatingIncome` cover continuing operations only, as GAAP presents them. For a company
that has sold or spun off a business, EPS from continuing operations is not served.

`bottomLineNetIncome` can differ from `netIncome` in both size and sign. HPE's fiscal 2025 is the
clear case: `netIncome` is +57,000,000 while `bottomLineNetIncome` is -59,000,000, alongside a
reported EPS of -0.0446. We publish both as the provider states them and make no claim about how
either reconciles with the EPS fields.

A weighted average is an average over the reporting period, not a count at period end. Neither
share field is a period-end share count, so neither is a correct input to a market capitalisation
or a book value per share.

> **Deprecated: `sharesOutstanding`**
>
> This field stops being populated on **2026-12-15**. It is not a shares-outstanding count despite
> its name, and it means different things on different responses: the diluted weighted average
> (or the basic one where no diluted figure exists) on history rows and on latest periods served
> from the statement source, a `marketCap / currentPrice` estimate on latest periods that carry
> market data, and null on fallback-source history rows and on some explicit-period requests.
> That ambiguity is why it is being retired rather than redefined.
>
> Migrate to `weightedAverageSharesDiluted`, falling back to `weightedAverageSharesBasic` when it
> is null. On every row where the old field was a share count, that gives the same number. It does
> not reproduce the `marketCap / currentPrice` estimate: if your code depends on that one, use
> `marketCap` and `currentPrice` directly.

---

### GET /fundamentals/historical/revenue

Returns a lightweight income-statement series (revenue, gross profit, operating income, net
income, EPS) per fiscal quarter or year, wrapped in `dataPoints` with `count` and `reason`
(`dataSource` is deprecated and always empty). Covers recent periods only (roughly the last three
to four years). For longer history or the balance sheet and cash flow, use `/fundamentals/history`.
`grossProfit` is null for a period whose cost of revenue fails the consistency check described
under `costOfRevenue` on `/fundamentals/history`. Where `/fundamentals/history` serves a figure
corrected from the company's filing (see Corrections from the filing under
[Reading the rows](#reading-the-rows)), this series serves the same figure for that period.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/fundamentals/historical/revenue?ticker=AAPL"
```

---

## Short Interest & Float

### GET /short-interest

Returns twice-monthly short interest data from FINRA for a stock, newest settlement date first.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/short-interest?ticker=AAPL&limit=24"
```

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker symbol |
| `limit` | integer | No | 24 | Maximum number of settlement dates to return |

**Response:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Uppercase ticker symbol |
| `count` | integer | Number of entries in `dataPoints` |
| `dataPoints[]` | array | One entry per settlement date, newest first |
| `dataPoints[].shortInterest` | number | Shares sold short and not yet covered, in shares |
| `dataPoints[].daysToCover` | number | `shortInterest` divided by `avgDailyVolume` |
| `dataPoints[].avgDailyVolume` | number | Average daily trading volume over the period, in shares |
| `dataPoints[].settlementDate` | string | Settlement date (ISO `YYYY-MM-DD`) |

```json
{
  "ticker": "AAPL",
  "dataPoints": [
    { "shortInterest": 120000000, "daysToCover": 2.5, "avgDailyVolume": 48000000, "settlementDate": "2026-01-15" }
  ],
  "count": 1
}
```

A ticker with no short interest data returns `200` with an empty `dataPoints` array and `count: 0`.

---

### GET /float

Returns the free float: shares available for public trading.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/float?ticker=AAPL"
```

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker symbol |

**Response:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Uppercase ticker symbol |
| `freeFloat` | number \| null | Free float, in shares. `0` means the symbol has no free float, as for funds such as SPY and QQQ |
| `freeFloatPercent` | number \| null | Free float as a percentage of total shares, in percentage points (`92.1` means 92.1%) |
| `effectiveDate` | string \| null | Date the float figure applies to, as `YYYY-MM-DD` or `YYYY-MM-DD HH:mm:ss` depending on the data source |

```json
{ "ticker": "AAPL", "freeFloat": 14000000000, "freeFloatPercent": 92.5, "effectiveDate": "2026-01-15" }
```

A ticker with no float data returns `200` with `freeFloat`, `freeFloatPercent` and `effectiveDate` all `null`. For share counts, use `weightedAverageSharesDiluted` and `weightedAverageSharesBasic` on [GET /fundamentals/history](#get-fundamentals-history).

---

### GET /short-volume

Returns daily short sale volume for a stock, newest date first.

```bash
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/short-volume?ticker=AAPL&limit=90"
```

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | string | Yes | - | Stock ticker symbol |
| `limit` | integer | No | 90 | Maximum number of trading days to return |

**Response:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Uppercase ticker symbol |
| `count` | integer | Number of entries in `dataPoints` |
| `dataPoints[]` | array | One entry per trading day, newest first |
| `dataPoints[].shortVolume` | number | Shares sold short that day, in shares |
| `dataPoints[].totalVolume` | number | Total shares traded that day on the reporting venues, in shares; not consolidated market volume |
| `dataPoints[].shortVolumeRatio` | number | `shortVolume` as a share of `totalVolume`, in percentage points (`45.2` means 45.2%) |
| `dataPoints[].date` | string | Trading day (ISO `YYYY-MM-DD`) |

```json
{
  "ticker": "AAPL",
  "dataPoints": [
    { "shortVolume": 7000000, "totalVolume": 15500000, "shortVolumeRatio": 45.16, "date": "2026-01-15" }
  ],
  "count": 1
}
```

A ticker with no short volume data returns `200` with an empty `dataPoints` array and `count: 0`.

---

## Quote Snapshot

### GET /{ticker}/quote

Returns a single-call aggregate snapshot combining the latest price (delayed 15 minutes), today's OHLC, 52-week range, market cap, and key fundamentals. Designed for detail pages that need all key stats in one request.

**Authentication:** API key required | **Rate limit:** standard quota

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/quote"
```

**Response object:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Ticker symbol |
| `currentPrice` | number\|null | Regular-session price, delayed 15 minutes. During RTH (09:30 to 16:00 ET): the most recent regular-session value. Otherwise: most recent regular-session close. |
| `change` | number\|null | `currentPrice` change vs `previousClose` |
| `changePercent` | number\|null | `currentPrice` change percentage vs `previousClose` |
| `volume` | number\|null | Volume for the current session |
| `open` | number\|null | Opening price for the current session |
| `dayHigh` | number\|null | Intraday high |
| `dayLow` | number\|null | Intraday low |
| `previousClose` | number\|null | Previous session close |
| `week52High` | number\|null | 52-week high |
| `week52Low` | number\|null | 52-week low |
| `marketCap` | number\|null | Market capitalization (USD) |
| `peRatio` | number\|null | Trailing P/E ratio |
| `epsTTM` | number\|null | Earnings per share (TTM) |
| `dividendYield` | number\|null | Annual dividend yield (decimal, e.g. `0.005`) |
| `movingAverage200Day` | number\|null | 200-day simple moving average of daily closes. `null` when fewer than 200 trading days of history exist (e.g. a recent IPO). |
| `reportedCurrency` | string\|null | Currency `epsTTM` is reported in ("USD", "EUR", "KRW", ...), as filed. Same field and same meaning as on the fundamentals endpoints. Absent when no fundamentals are available for the ticker. |
| `timestamp` | number\|null | When this response was served (epoch milliseconds). Not the age of the price: it tracks the current clock regardless of how old the value is. |
| `priceAsOf` | number \| absent | When the market data behind `currentPrice` is from (epoch milliseconds). This is the field to read for freshness. Omitted outside regular hours, and whenever the upstream data is not dated, so treat its absence as "unknown" rather than "fresh". |
| `extendedHours` | object \| null | Extended-hours view (pre-market or after-hours). Absent during RTH, overnight, and weekends. Same shape as on `/price`: `{ session, price, change, changePercent }`. |
| `listingStatus` | string \| absent | `"DELISTED"` or `"PENDING_DELISTING"`. Present only when the symbol is delisted or pending delisting, absent otherwise. |
| `delistedDate` | string \| absent | ISO date (YYYY-MM-DD) trading stopped. Present only alongside `listingStatus: "DELISTED"`. |
| `delistingReason` | string \| absent | One of `acquired`, `take_private`, `bankruptcy`, `exchange_rule`, `merged`. Present only alongside `listingStatus: "DELISTED"`. |

All fields except `ticker` are nullable. Render "--" or hide the row when a field is absent.

**Null fields are omitted from the JSON.** A null field is left out of the response body entirely rather than serialized as `null`, so the key count varies by ticker. Read defensively rather than assuming a key is present.

**Foreign filers omit `reportedCurrency`, `epsTTM` and `peRatio`.** On foreign ADR filers such as `ASML` and `TM` those three fields are absent, while price fields, `marketCap` and `dividendYield` are served normally. This is the same cross-currency rule the [fundamentals endpoints](#get-fundamentals) follow: `currentPrice` is the USD ADR price and the filer's earnings are in home currency, so a trailing P/E would mix two currencies. We withhold it rather than publish a wrong number. Do not fill the gap by dividing `currentPrice` by a non-USD `epsTTM`.

**Delisted symbols keep quoting their last trade.** When `listingStatus` reads `"DELISTED"`, every price field above (including `open`, `dayHigh`, `dayLow`, `previousClose` and `volume`) describes the final trading session. `priceAsOf` is the close of that session, which can fall a day or more before `delistedDate`, and `extendedHours` is absent. `timestamp` remains the serve time, so do not read it as the age of the price. The same three listing fields appear on [GET /price](#get-price) and [GET /{ticker}/profile](#get-profile).

**Example response (after-hours):**

```json
{
  "ticker": "AAPL",
  "currentPrice": 213.45,
  "change": 1.23,
  "changePercent": 0.58,
  "volume": 48203100,
  "open": 212.10,
  "dayHigh": 214.20,
  "dayLow": 211.80,
  "previousClose": 212.22,
  "week52High": 237.23,
  "week52Low": 164.08,
  "marketCap": 3280000000000,
  "peRatio": 32.1,
  "epsTTM": 6.65,
  "dividendYield": 0.0044,
  "movingAverage200Day": 198.42,
  "reportedCurrency": "USD",
  "timestamp": 1745600000000,
  "extendedHours": {
    "session": "post",
    "price": 214.10,
    "change": 0.65,
    "changePercent": 0.30
  }
}
```

**ETF tickers:** This endpoint is stock-only. Calling it with an ETF ticker (e.g. `VTI`, `SPY`) returns `400 ticker_is_etf` with a pointer to [GET /etfs/{ticker}/quote](/docs/api/etfs/#get-etf-quote) which returns AUM, expense ratio, NAV, and inception date instead of market cap / P/E / EPS. The error body also carries a `seeInstead` array holding that exact path, so a client can follow it without string-matching the message:

```json
{
  "error": "ticker_is_etf",
  "message": "SPY is an ETF; use /api/v1/etfs/SPY/quote",
  "seeInstead": ["/api/v1/etfs/SPY/quote"]
}
```

**Rate-limit note:** Cached for 15 seconds server-side. Each call still counts toward your monthly quota.

---

## Company KPIs

A KPI is a per-company key performance indicator series: the operating numbers a company reports about itself, quarter after quarter, in a chartable shape. Financial statements describe what every company reports; KPIs describe what a specific company reports. The set covers GAAP line items and the company-specific non-GAAP metrics that move a stock, such as iPhone unit sales, Tesla deliveries, AWS revenue, and Netflix paid net adds. Every series is curated per company from earnings filings and press releases, and carries a citation to the filing it came from.

The endpoints below cover the whole surface: [GET /with-kpis](#list-kpi-coverage) enumerates coverage, [GET /{ticker}/kpis/types](#get-kpi-types) lists one company's metrics, and [GET /{ticker}/kpis](#get-kpis) returns the series. The reference for each follows this section.

Worked examples: [the Company KPI API guide](/blog/company-kpi-api-segment-revenue-non-gaap-metrics/).

### The three-step KPI workflow

Discovery first, then metadata, then data. Steps 1 and 2 are Discovery tier: an API key is required for identity, but neither call consumes monthly quota, so a client can refresh the shape of its universe far more often than it fetches series payloads. Only step 3 is quota-gated and PRO-gated.

**Step 1: discover which companies have coverage.** Call once at startup and cache it. Enumerating coverage is the supported alternative to probing tickers one at a time and collecting `404`s.

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/with-kpis"
```

```json
{
  "count": 947,
  "tickers": [
    { "ticker": "AAPL", "companyName": "Apple Inc.", "lastUpdated": "2026-01-30", "kpiCount": 8 },
    { "ticker": "ABBV", "companyName": "AbbVie Inc.", "lastUpdated": "2026-01-25", "kpiCount": 6 }
  ]
}
```

**Step 2: list one company's KPI types.** Metadata tuples only, with no series payload attached. Use it to decide what is worth rendering, or to let an agent choose, before committing to the heavy call.

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/kpis/types"
```

```json
[
  { "id": "iphone_revenue", "name": "iPhone Revenue", "category": "product_revenue", "chartType": "bar" },
  { "id": "services_revenue", "name": "Services Revenue", "category": "segment_revenue", "chartType": "line" }
]
```

**Step 3: fetch the series.** The endpoint takes only the ticker and returns every curated series for it; select the ones you need by `id` from Step 2. Points are ordered newest to oldest: the first element is the most recently reported period.

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/kpis"
```

```json
{
  "isPreview": false,
  "previewReason": null,
  "data": {
    "ticker": "AAPL",
    "companyName": "Apple Inc.",
    "cik": "0000320193",
    "lastUpdated": "2026-01-30",
    "kpis": [
      {
        "id": "iphone_revenue",
        "name": "iPhone Revenue",
        "category": "product_revenue",
        "unit": "USD",
        "displayFormat": "currency_abbreviated",
        "chartType": "bar",
        "values": [
          { "period": "Q4 FY2025", "date": "2025-09-27", "value": 50000000000, "isEstimate": null },
          { "period": "Q1 FY2026", "date": "2025-12-27", "value": 85000000000, "isEstimate": null }
        ],
        "sourceRef": "Apple 8-K Q1 FY2026 press release",
        "discontinued": false,
        "discontinuedNote": null
      }
    ]
  }
}
```

Example values throughout this section are illustrative, not live readings.

---

### GET /{ticker}/kpis

Returns company-specific KPI time-series for a ticker. KPIs are curated GAAP and non-GAAP metrics extracted from earnings filings and press releases (e.g. iPhone unit sales, Tesla deliveries, AWS revenue, Netflix paid net adds).

**Authentication:** API key required on every call; PRO for the series data. A free key receives `isPreview: true` with the company metadata and an empty `kpis` list, which is enough to detect coverage and gate the UI. Calls without a key return `401 api_key_required`.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker symbol |

**Coverage today:** near-complete for the S&P 500 plus an extended universe of 900+ tickers, and growing. Entries can briefly show `kpiCount: 0` while curation for a newly added company is in flight; skip those when enumerating. **Roadmap:** coverage extending to the long-tail of US tickers; some tickers may return `404 Not Found` until curated. Use [GET /with-kpis](#list-kpi-coverage) to enumerate the current set.

**Example Request:**

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/kpis"
```

**Response Schema (envelope):**

| Field | Type | Description |
|-------|------|-------------|
| `isPreview` | boolean | `true` when the caller is on the FREE tier |
| `previewReason` | string | `"PRO_REQUIRED"` or `null` |
| `upgrade` | object | Present only when `isPreview` is `true`. Carries `plan`, `message`, `price`, `url` and `relay`: surface `message` and `url` to your user in one line, then continue with the preview `data` |
| `data.ticker` | string | Stock ticker |
| `data.companyName` | string | Company name |
| `data.cik` | string | SEC Central Index Key (when available) |
| `data.lastUpdated` | string | ISO date of the last KPI refresh |
| `data.kpis` | array of `KpiSeries` | Time-series objects (PRO only; empty in preview) |

**KpiSeries object:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Stable per-ticker identifier, e.g. `"iphone_revenue"` |
| `name` | string | Human-readable name, e.g. `"iPhone Revenue"` |
| `category` | string | Logical category: `"product_revenue"`, `"segment_revenue"`, `"unit_economics"`, etc. |
| `unit` | string | Unit of measurement: `"USD"`, `"units"`, `"subscribers"`, `"%"`, etc. |
| `displayFormat` | string | Display hint: `"currency_abbreviated"`, `"number_abbreviated"`, `"percent"`, etc. |
| `chartType` | string | Default chart type: `"bar"` or `"line"` |
| `values` | array of `KpiDataPoint` | Time-series data points, newest to oldest (first element is most recent) |
| `sourceRef` | string | Citation for the source filing |
| `discontinued` | boolean or null | `true` when the company has stopped reporting this metric; `null` or `false` otherwise |
| `discontinuedNote` | string | Optional human-readable note about discontinuation |

**KpiDataPoint object:**

| Field | Type | Description |
|-------|------|-------------|
| `period` | string | Fiscal period label, e.g. `"Q2 FY2026"` |
| `date` | string | ISO date of the period close, e.g. `"2025-12-27"` |
| `value` | number | Numeric value for the period |
| `isEstimate` | boolean\|null | `true` for preliminary or estimated values; usually null |

**Example Response (truncated):**

```json
{
  "isPreview": false,
  "previewReason": null,
  "data": {
    "ticker": "AAPL",
    "companyName": "Apple Inc.",
    "cik": "0000320193",
    "lastUpdated": "2026-04-30",
    "kpis": [
      {
        "id": "iphone_revenue",
        "name": "iPhone Revenue",
        "category": "product_revenue",
        "unit": "USD",
        "displayFormat": "currency_abbreviated",
        "chartType": "bar",
        "values": [
          { "period": "Q1 FY2025", "date": "2024-12-28", "value": 69702000000, "isEstimate": null },
          { "period": "Q2 FY2025", "date": "2025-03-29", "value": 46841000000, "isEstimate": null },
          { "period": "Q3 FY2025", "date": "2025-06-28", "value": 39286000000, "isEstimate": null },
          { "period": "Q4 FY2025", "date": "2025-09-27", "value": 49025000000, "isEstimate": null },
          { "period": "Q1 FY2026", "date": "2025-12-27", "value": 85269000000, "isEstimate": null }
        ],
        "sourceRef": "Apple 8-K Q1 FY2026 press release",
        "discontinued": false,
        "discontinuedNote": null
      }
    ]
  }
}
```

---

### GET /with-kpis

Lists every ticker with curated KPI coverage, sorted alphabetically. Builder discovery use case: render a supported-tickers page or seed a watchlist without 404-probing one ticker at a time.

**Authentication:** API key required. **No monthly quota cost** (Discovery tier: the call registers identity for abuse tracking but does not burn the per-month quota; rate-limit-per-minute still applies).

**Parameters:** None.

**Example Request:**

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/with-kpis"
```

**Response Schema:**

| Field | Type | Description |
|-------|------|-------------|
| `count` | int | Total number of tickers with curated KPI coverage |
| `tickers` | array of `KpiCoverageEntry` | Listing sorted alphabetically by ticker |

**KpiCoverageEntry object:**

| Field | Type | Description |
|-------|------|-------------|
| `ticker` | string | Stock ticker |
| `companyName` | string | Company name |
| `lastUpdated` | string | ISO date of the last KPI refresh for this ticker |
| `kpiCount` | int | Number of distinct KPI series available for this ticker |

**Example Response (truncated):**

```json
{
  "count": 947,
  "tickers": [
    { "ticker": "A",    "companyName": "Agilent Technologies, Inc.", "lastUpdated": "2026-04-12", "kpiCount": 5 },
    { "ticker": "AAPL", "companyName": "Apple Inc.",                 "lastUpdated": "2026-04-30", "kpiCount": 8 },
    { "ticker": "ABBV", "companyName": "AbbVie Inc.",                "lastUpdated": "2026-04-22", "kpiCount": 6 }
  ]
}
```

---

### GET /{ticker}/kpis/types

Lists the KPI metadata tuples available for a ticker without paying the cost of the full series payload. Mirrors the `/api/v1/insights/stock/{ticker}/types` precedent. Useful for letting an agent or UI decide which KPIs to render before fetching the data.

**Authentication:** API key required. **No monthly quota cost** (Discovery tier).

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker symbol |

**Example Request:**

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/kpis/types"
```

**Response Schema (bare array):**

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Stable per-ticker identifier |
| `name` | string | Human-readable name |
| `category` | string | Logical category |
| `chartType` | string | Default chart type: `"bar"` or `"line"` |

**Example Response:**

```json
[
  { "id": "iphone_revenue",   "name": "iPhone Revenue",   "category": "product_revenue", "chartType": "bar"  },
  { "id": "services_revenue", "name": "Services Revenue", "category": "segment_revenue", "chartType": "line" },
  { "id": "mac_revenue",      "name": "Mac Revenue",      "category": "product_revenue", "chartType": "bar"  }
]
```

Returns `404 Not Found` when the ticker has no curated KPIs.

---

### Reading a KPI point

**Periods are fiscal, not calendar.** `period` is a display label such as `"Q1 FY2026"`; `date` is the ISO date on which that period closed. The two routinely disagree with the calendar: a fiscal Q1 can close in December. Sort and align on `date`, label with `period`, and do not assume a fiscal quarter maps to a calendar quarter when comparing two companies.

**Units live on the series, not on the point.** `value` is a raw number. Its meaning comes from `unit` on the parent series (`"USD"`, `"units"`, `"subscribers"`, `"%"`), and `displayFormat` is the matching rendering hint (`currency_abbreviated`, `number_abbreviated`, `percent`). Read the two together rather than inferring a unit from the series name. `chartType` carries the intended default rendering, `bar` or `line`.

**Points run newest to oldest.** The first element is the most recently reported period, so a sequential delta is the first two elements, and a year-over-year delta is the first element against the one four positions later (index 0 against index 4) when the series has no gaps. Series lengths vary by company and metric; check the length before indexing.

**Two flags change how a series should be displayed.** `isEstimate` is `true` for a preliminary or estimated value and is usually `null` otherwise. `discontinued` is `true` once a company stops reporting a metric, with an optional `discontinuedNote`; a discontinued series remains valid history but should not sit on a live dashboard as though a new print is coming.

**Every series carries `sourceRef`,** a citation for the filing the numbers came from. Surface it wherever a user might ask where a number originated.

**Dual-class tickers resolve server-side.** KPI endpoints accept the secondary share class and resolve it to the curated primary, so `GOOG` reaches Alphabet's series and `BRK-B` reaches Berkshire's. See [Ticker symbols](/docs/api/#ticker-symbols) for the aliasing rules, including the pairs that are tracked separately rather than aliased.

### Patterns for builders

**Quarter-over-quarter and year-over-year deltas.** The series arrives ordered newest first, so deltas are arithmetic rather than an extra request: the first two points for the sequential move, the first against the point four positions later for the annual move. Ranking a watchlist by acceleration in a company-specific metric surfaces moves that a price screen does not.

**Earnings-eve reads.** Combine [GET /api/v1/calendar/earnings](/docs/api/calendar/) with a watchlist to find which companies report next, then load their KPI series to show which operating numbers matter and how they have trended into the print. Because coverage discovery and KPI types cost no monthly quota, the watchlist intersection can refresh far more often than the series fetch.

**Pairing KPIs with sentiment.** Fetch the reported operating number from the KPI endpoints and the market's reading from [GET /api/v2/metrics/entity/{entityId}/metric/{metricType}](/docs/api/metrics/), where `metricType` is `mentions`, `sentiment`, `sentisense`, or `social_dominance`. Metric points are time-ascending with a flat `value` scalar, so they plot directly against a KPI series. A core metric accelerating while sentiment fades, or the reverse, is a divergence a price chart does not show.

**Cache by reporting cadence.** Most KPI series change only when a company files, which for the majority of metrics means once a quarter. Cache the coverage list and the type list on a daily cadence, and refresh a company's series around its reporting date rather than on every page view. `lastUpdated` on both the coverage entry and the series payload tells you when a refresh actually landed.

---

## Sentiment

### GET /{ticker}/sentiment

Returns the stock's headline sentiment picture in one call: the proprietary SentiSense Score, its
30-day regime and direction, a daily sparkline, where the conversation is happening by source, the
stories driving it, and curated peer tickers.

This is the endpoint the Developer Console hands you as your first call.

**Authentication:** API key required. Available in full on the Free tier.

**Parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `ticker` | path | Yes | - | Stock ticker symbol |

**Example Request:**

```bash
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/sentiment"
```

**When there is nothing to return:** a `404` whose `error` is either `entity_not_found` (no stock is tracked under that symbol; the body carries up to three `suggestions`, so resolve with `GET /api/v1/kb/entities/search?q=` and retry) or `no_coverage` (the symbol is tracked, we hold no sentiment record for it, and a different spelling will not help). See [Two kinds of 404](/docs/api/#two-kinds-of-404-on-a-per-ticker-endpoint).

**Response:** `{ isPreview, previewReason, data }`. Every field below lives under `data`. The endpoint is available in full on every tier and still uses the wrapper, so an API-key caller always gets `isPreview: false` and `previewReason: null`. Unwrap first: the Score is at `data.sentisenseScore`, and reading `sentisenseScore` off the root returns nothing.

**Response Schema:**

| Field (under `data`) | Type | Description |
|-------|------|-------------|
| `ticker` | string | Ticker symbol |
| `companyName` | string | Company name |
| `asOf` | string | ISO date (`YYYY-MM-DD`) the data is current as of |
| `sentisenseScore` | number or null | Today's SentiSense Score (sentiment x mentions composite, unbounded). Null until today's reading lands, see the note below |
| `sentisenseScoreAvg30d` | number | 30-day average Score, the stable regime figure |
| `sentisenseScoreDelta30d` | number | Change in the Score over 30 days |
| `scoreLabel` | string | Seven-band label of the 30-day average: `"Strong Bearish"`, `"Bearish"`, `"Slightly Bearish"`, `"Neutral"`, `"Slightly Bullish"`, `"Bullish"` or `"Strong Bullish"` |
| `direction` | string | `"Bullish"`, `"Neutral"` or `"Bearish"`, from the 30-day average |
| `latestDirection` | string or null | Same three bands, from today's read. Null in lockstep with `sentisenseScore` |
| `trend` | string | `"UP"`, `"DOWN"` or `"FLAT"` |
| `scoreSparkline` | number[] | Daily Score series |
| `mentions` | number or null | Mention volume of the latest New York daily bucket with data: today's once it lands, otherwise an earlier day, which `narrative` and `faq` name; null when there is none |
| `mentionsAvg30d` | number | 30-day average mentions per day |
| `socialDominance` | number | Latest share of voice, as a fraction (`0.021` = 2.1%) |
| `bySource[]` | array | Per-source tone, loudest source first: `source`, `direction`, `mentionShare` (whole-number percent; each share is rounded independently, so the array can sum to 99 or 101), `value` (exact polarity, `-1` to `1`) |
| `relatedTickers[]` | array | Curated peer tickers: `ticker`, `name` |
| `drivers[]` | array | Top story drivers: `title`, `tone` (`-1` to `1`) |
| `narrative` | string | Short plain-language summary of why the Score sits where it does |
| `faq[]` | array | Question and answer pairs covering the common asks for this ticker |

**`sentisenseScore` and `latestDirection` are today's reading, and are `null` until the day's first analytics run lands** (mid-morning ET, later at weekends). Poll before that and every ticker returns null for these two, which is a timing state and not an outage. The rest of the response is unaffected: `scoreLabel`, `direction` and `sentisenseScoreAvg30d` are all computed from the 30-day average, so prefer those when you need a headline that is always present. A null here means "no reading yet", never a Score of zero. A measured 0.0 is served as `0.0`, so do not coerce null to 0, and do not infer absence by thresholding the 30-day average, which would suppress genuine neutrals.

**Example Response:**

```json
{
  "isPreview": false,
  "previewReason": null,
  "data": {
    "ticker": "AAPL",
    "companyName": "Apple Inc.",
    "asOf": "2026-08-02",
    "sentisenseScore": 41.2,
    "sentisenseScoreAvg30d": 33.8,
    "sentisenseScoreDelta30d": 7.4,
    "scoreLabel": "Bullish",
    "direction": "Bullish",
    "latestDirection": "Bullish",
    "trend": "UP",
    "scoreSparkline": [28.1, 30.4, 31.9, 35.2, 41.2],
    "mentions": 1840,
    "mentionsAvg30d": 1512,
    "socialDominance": 0.081,
    "bySource": [
      { "source": "News",   "direction": "Bullish", "mentionShare": 58, "value": 0.1889 },
      { "source": "Reddit", "direction": "Neutral", "mentionShare": 21, "value": 0.0428 },
      { "source": "X",      "direction": "Bullish", "mentionShare": 10, "value": 0.1809 }
    ],
    "relatedTickers": [ { "ticker": "MSFT", "name": "Microsoft Corporation" } ],
    "drivers": [ { "title": "Apple raises services guidance", "tone": 0.44 } ],
    "narrative": "Sentiment has strengthened over the past month, led by news coverage.",
    "faq": [ { "question": "Is AAPL bullish or bearish?", "answer": "..." } ]
  }
}
```

Returns `404 Not Found` when the ticker has no sentiment coverage.

This is the single-stock read. Two market-wide counterparts live in the [Market Mood API](/docs/api/market-mood/): `/api/v1/sentiment/sectors` ranks the 11 GICS sectors against the market's own tone, and `/api/v1/sentiment/breadth` gives the bullish/neutral/bearish share of the roughly 1,000 covered stocks, the sentiment analogue of an advance/decline line.

**Note on the response envelope:** search-engine crawlers receive the same envelope with
`isPreview: true` and each per-source `value` set to `null`; direction and mention share are always
present. API-key callers on any tier receive the full payload with `isPreview: false`.
