# GET /v2/events

**Query events**

One timeline across every ticker: earnings, dividends, splits, insider filings, analyst actions, plus opt-in signal firings and news.

## Query / path parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `kind` | query | string[] | no | Comma list of kinds. Omitted → the five corporate kinds; `signal` and `news` join only when named here. Enum: `earnings`, `dividend`, `split`, `insider`, `analyst`, `news`, `signal`. Example: `split,analyst`. |
| `ticker` | query | string | no | Single-ticker filter. When both `ticker` and `tickers` are passed, `ticker` wins. Example: `AAPL`. |
| `tickers` | query | string[] | no | Comma list of tickers (max 50). Mutually exclusive with `universe`. Example: `AAPL,NVDA,TSLA`. |
| `universe` | query | string | no | Universe slug (`top_10`, `top_100`, or one of yours) to scope the stream. Mutually exclusive with `tickers`. Example: `top_100`. |
| `firm` | query | string | no | Analyst-only structured filter — requires `kind=analyst` alone (`400` otherwise). Exact firm-name match on the ratings feed. Example: `Morgan Stanley`. |
| `action` | query | string | no | Analyst-only structured filter — requires `kind=analyst` alone. Same `action` vocabulary as [Analyst actions](/docs/endpoints/events/analyst). Enum: `upgrades`, `downgrades`, `initiates_coverage_on`, `maintains`, `reiterates`, `assumes`, `reinstates`, `suspends`, `terminates_coverage_on`. Example: `downgrades`. |
| `signal` | query | string | no | Signal-only filter — requires `kind=signal` alone (`400` otherwise). One built-in boolean signal; REQUIRED with `q` or `join=state` on that kind. See [Signal firings](/docs/endpoints/events/signal-firings). Example: `golden_cross`. |
| `transition` | query | string | no | Signal-only filter — requires `kind=signal` alone. `enter` (false→true) or `exit` (true→false). Enum: `enter`, `exit`. Example: `enter`. |
| `from` | query | string | no | Events at/after this instant — strict ISO: `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM[:SS]Z`. A bare `YYYY-MM-DD` means from the start of that day. (`since` accepted as an alias.) Example: `2026-08-27`. |
| `to` | query | string | no | Window end — same strict ISO subset. A bare `YYYY-MM-DD` means through the end of that day, matching bars/series/spans; a timestamp is exclusive (events strictly before it). (`until` accepted as an alias.) Example: `2026-06-28`. |
| `q` | query | string | no | SQL WHERE over the projection — `ticker`, `ts`, `kind`, `payload` (plus ticker-state signals when `join=state`). When exactly ONE `kind` is named, that kind's payload fields are additionally first-class typed columns (`amount > 1`, `firm = 'Goldman Sachs'` — see each kind page for its list); multi-kind requests use `payload->>'…'`. Max 4000 chars. ANDs with the filter params. Example: `kind='analyst' AND payload->>'action'='downgrades'`. |
| `join` | query | string | no | Set to `state` to allow ticker-state signals in `q`/`select`/`group_by`/`having`, evaluated as of each event's timestamp (daily resolution). Enum: `state`. |
| `interval` | query | string | no | Grain the per-event state is reconstructed at, when `join=state`: `1m`, `1h`, `1d`, or `auto` (default). `auto` resolves to `1d` — the event set's tickers are not known before the query runs, and `1d` is the only tier covering the whole universe, so it is the only grain guaranteed to satisfy every event. An explicit `1m`/`1h` trades coverage for precision: events on tickers absent from that tier join to `null`. A referenced column the grain does not store is a `400`. Reported back as `_meta.state_interval`. Enum: `1m`, `1h`, `1d`, `auto`. Default: `auto`. |
| `select` | query | string[] | no | Aggregate-mode output columns (requires `group_by`). Default: group keys + `COUNT(*) AS events`. Same naming rule as `group_by` — alias with `AS`, or take the name derived for you. Example: `kind, COUNT(*) AS events`. |
| `group_by` | query | string[] | no | Comma list of rollup keys — switches the response to aggregate rows. Columns (`kind`, `ticker`), payload fields (`firm`, or the explicit `payload->>'firm'`), and expressions over them all roll up. Name a key with `AS` to choose its JSON key: `payload->>'firm' AS firm`. Un-named keys are named for you — a payload read takes its key (`payload->>'firm'` → `firm`), a function keeps the function's name (`lower(ticker)` → `lower`), and anything else falls back to `group_1`, `group_2`. Example: `kind`. |
| `having` | query | string | no | Post-aggregation filter. Requires `group_by`. Example: `COUNT(*) > 5`. |
| `order` | query | string | no | Aggregate-mode sort — a bare column name or an output name only (put expressions in `select` and sort by their alias). A group key's name works too, whether you aliased it or it was named for you: `group_by=payload->>'firm' AS firm&order=firm`. Default: `events`. (Row mode is always newest-first.) Example: `events`. |
| `dir` | query | string | no | Aggregate-mode sort direction. Enum: `asc`, `desc`. Default: `desc`. |
| `limit` | query | integer | no | Page size (row modes) / max rollup rows (aggregate mode). Max 1000. Default: `50`. Example: `5`. |
| `cursor` | query | string | no | Opaque cursor from the previous response — carries the original filters (and `q` when short), so pass it alone. Not valid with `group_by`. |

## Returns

- `as_of` (string) — Server time this response was assembled (ISO 8601).
- `query` (object) — Your filters, echoed exactly as you sent them — `q`, `select`, `group_by` and `having` come back in your spelling, not the SQL they compile to — including `join` and its grain when you passed `join=state`.
- `count` (number) — Rows in this page.
- `next_cursor` (string) — Opaque token for the next page; `null` on the last page. Carries `q_truncated: true` alongside it when an oversized `q` could not ride the token — resend `q` on later pages.
- `truncated` (boolean) — Aggregate mode only (`group_by`): `true` when the rollup stopped at its row cap. Aggregate responses are unpaged, so `next_cursor` is absent there.
- `results` (array) — One row per event (`ticker`, `ts`, `kind`, `payload`), or rollup rows plus `truncated: true` when an aggregate exceeds `limit`.
- `_meta` (object) — With `join=state` only: `state_interval`, the grain the joined state was read at, and `deprecated_columns` when a state column was named under its pre-2026-09-07 spelling and served under its current name (`requested`, `use`, `note`).

## Status codes

- **200** — Success — the response shape is documented under Returns above.
- **400** — Bad `kind`, >50 tickers, `tickers`+`universe` together, no bound at all, `kind=signal` with `q`/`join=state` but without `signal=` (or with `group_by` at all), invalid timestamps/cursor, or `invalid_query` — an identifier outside the projection (or outside the widened set under `join=state`), a disallowed function, `select`/`having`/`order` without `group_by`, or `cursor` with `group_by`.
- **404** — Referenced `universe` does not exist.
- **503** — `kind_unavailable` — the `signal` event log is temporarily unavailable; retry later. The other kinds are unaffected. Validation runs first, so a 503 means the query itself is well-formed — a request that would 400 still 400s while the log is down.

## Sample response

```json
{
  "as_of": "2026-08-11T23:02:36.722Z",
  "query": { "kind": "split,analyst", "tickers": null, "universe": "top_100", "since": "2026-07-12T00:00:00.000Z", "until": null, "limit": 2 },
  "count": 2,
  "next_cursor": "eyJ2IjoxLCJwb3MiOnsiYW5hbHlzdCI6eyJ0IjoxNzg2NDc5MDEwMDAwLCJrIjoiQU1BVCI…",
  "results": [
    { "ticker": "ASTS", "ts": "2026-08-11T20:54:44.000Z", "kind": "analyst",
      "payload": { "event_id": "6a7b8c14e48561000117ed78", "firm": "UBS", "analyst": "Christopher Schoell", "action": "maintains", "rating": "neutral", "previous_rating": "neutral", "price_target": 78, "previous_price_target": 80, "price_target_action": "lowers", "importance": 4 } },
    "… (one row per event, newest first)"
  ]
}
```

## More examples

### Event volume by kind (aggregate mode)

Request:

```shell
curl -G "https://api.tickerbot.io/v2/events" \
  -H "Authorization: Bearer YOUR_KEY" \
  --data-urlencode "kind=analyst,split" \
  --data-urlencode "from=2026-08-17" \
  --data-urlencode "group_by=kind"
```

Response (`200`):

```json
{
  "as_of": "2026-08-11T23:05:43.031Z",
  "query": { "kind": "analyst,split", "tickers": null, "universe": null, "since": "2026-07-12T00:00:00.000Z", "until": null, "q": null, "select": null, "group_by": ["kind"], "having": null, "order": "events", "dir": "desc", "limit": 50 },
  "count": 2,
  "results": [
    { "kind": "analyst", "events": 9370 },
    { "kind": "split", "events": 145 }
  ]
}
```

### Downgrade volume by firm (rolling up a payload field)

Request:

```shell
curl -G "https://api.tickerbot.io/v2/events" \
  -H "Authorization: Bearer YOUR_KEY" \
  --data-urlencode "kind=analyst" \
  --data-urlencode "from=2026-08-17" \
  --data-urlencode "q=action='downgrades'" \
  --data-urlencode "group_by=payload->>'firm' AS firm" \
  --data-urlencode "having=COUNT(*) > 5" \
  --data-urlencode "limit=5"
```

Response (`200`):

```json
{
  "as_of": "2026-08-12T16:54:39.884Z",
  "query": { "kind": "analyst", "tickers": null, "universe": null, "since": "2026-07-03T00:00:00.000Z", "until": null, "q": "action='downgrades'", "select": null, "group_by": ["payload->>'firm' AS firm"], "having": "COUNT(*) > 5", "order": "events", "dir": "desc", "limit": 5 },
  "count": 5,
  "truncated": true,
  "results": [
    { "firm": "Morgan Stanley", "events": 42 },
    { "firm": "JP Morgan", "events": 29 },
    { "firm": "Barclays", "events": 26 },
    { "firm": "Wells Fargo", "events": 21 },
    { "firm": "Jefferies", "events": 20 }
  ]
}
```

### Same rollup, un-named key — the name is derived for you

Request:

```shell
curl -G "https://api.tickerbot.io/v2/events" \
  -H "Authorization: Bearer YOUR_KEY" \
  --data-urlencode "kind=analyst" \
  --data-urlencode "from=2026-08-17" \
  --data-urlencode "group_by=firm" \
  --data-urlencode "limit=3"
```

Response (`200`):

```json
{
  "as_of": "2026-08-12T16:54:42.769Z",
  "query": { "kind": "analyst", "tickers": null, "universe": null, "since": "2026-07-03T00:00:00.000Z", "until": null, "q": null, "select": null, "group_by": ["firm"], "having": null, "order": "events", "dir": "desc", "limit": 3 },
  "count": 3,
  "truncated": true,
  "results": [
    { "firm": "Barclays", "events": 777 },
    { "firm": "Wells Fargo", "events": 702 },
    { "firm": "UBS", "events": 669 }
  ]
}
```

### join=state: downgrades on stocks above their 200-day (point-in-time)

Request:

```shell
curl -G "https://api.tickerbot.io/v2/events" \
  -H "Authorization: Bearer YOUR_KEY" \
  --data-urlencode "kind=analyst" \
  --data-urlencode "join=state" \
  --data-urlencode "from=2026-08-17" \
  --data-urlencode "q=payload->>'action'='downgrades' AND above_sma_200 = true"
```

Response (`200`):

```json
{
  "as_of": "2026-08-11T23:02:40.660Z",
  "query": { "kind": "analyst", "tickers": null, "universe": null, "since": "2026-07-12T00:00:00.000Z", "until": null, "q": "payload->>'action'='downgrades' AND above_sma_200 = true", "limit": 2, "join": "state", "state_resolution": "1d" },
  "_meta": { "state_interval": "1d" },
  "count": 50,
  "next_cursor": "eyJmIjp7ImtpbmQiOiJhbmFseXN0In19",
  "results": [
    { "ticker": "CZR", "ts": "2026-07-29T12:04:00.000Z", "kind": "analyst",
      "payload": { "firm": "TD Cowen", "action": "downgrades", "rating": "Hold", "previous_rating": "Buy", "price_target": 32, "previous_price_target": 51 } },
    "(...)"
  ]
}
```

## Notes

- Rows are `{ ticker, ts, kind, payload }`, newest first. Payload fields per kind match the legacy [`/v2/tickers/{ticker}/events`](/docs/endpoints/tickers/events) and `/v2/analyst/events` spellings, so switching here is a URL swap, not a reshape — and both of those are deprecated with a 2026-10-31 sunset, with this endpoint as their successor.
- The default stream is the five corporate kinds, each backed by its own archive: [`earnings`](/docs/endpoints/events/earnings), [`dividend`](/docs/endpoints/events/dividends), [`split`](/docs/endpoints/events/splits), [`insider`](/docs/endpoints/events/insider), [`analyst`](/docs/endpoints/events/analyst). Two more join as **explicit opt-ins**, named in `kind`: [`signal`](/docs/endpoints/events/signal-firings) — boolean firings as enter/exit point-events — and `news`, mirroring [`/v2/news`](/docs/endpoints/news/query).
- **The endpoint speaks the SQL grammar** over a normalized projection of exactly four columns — `ticker`, `ts`, `kind`, `payload` (jsonb). `q` filters rows (`kind='analyst' AND payload->>'action'='downgrades'`); `group_by` with optional `select`/`having` switches to aggregate rollups. Payload fields are reached with jsonb operators and cast as needed: `(payload->>'shares')::numeric > 1e6`. No other identifiers are accepted — this is the event log's own grammar, not the ticker-state scan grammar.
- **`join=state`** widens the grammar to ticker-STATE signals, evaluated against each event's ticker as of the event's timestamp. "Downgrades on stocks above their 200-day" is one call: `kind=analyst&join=state&q=payload->>'action'='downgrades' AND above_sma_200 = true`. State resolves at daily resolution; the `query` echo carries `join: "state"` with the deprecated `state_resolution: "1d"`, and `_meta.state_interval` is the canonical grain field. Works in row and aggregate mode.
- Requires at least one bound — a ticker scope or a time window; `q` alone does not count, and a bare `kind` over millions of rows is refused.
- `kind=signal` needs the signal **named** before it will take the grammar: `q` and `join=state` over the ~175M-row firing log require `signal=<name>`, which is what lets the query ride an index instead of scanning (`transition=enter|exit` is optional). `group_by` over that kind is unavailable — an aggregate has no `limit` to stop at, so it reads every firing in the window. Filter mode needs none of this.
- Aggregates do not paginate: past `limit` you get `truncated: true` — narrow the window instead.
- The cursor pages strictly *older* than the last row's timestamp, and the corporate archives are date-grained, so same-date remainders can fall on a page boundary. Raise `limit` (max 1000) to avoid them. A `q` too long to ride inside the cursor token sets `q_truncated: true` — resend `q` alongside `cursor` on later pages.
- Both `GET` and `POST` are supported; `POST` takes the same parameters in the body for long queries.

---

Interactive sandbox + parameter editor: https://tickerbot.io/docs/endpoints/events/query
