# GET /v2/events?kind=signal

**Signal firings**

The moments a built-in boolean flipped, as enter/exit point-events. Opt-in: name `signal` in `kind` and it joins the stream.

## Query / path parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `kind` | query | string | yes | Include `signal` explicitly — it never joins an unfiltered stream. Example: `signal`. |
| `signal` | query | string | no | One built-in boolean signal. Optional in filter mode; REQUIRED with `q` or `join=state`, which is what keeps the query on an index. An unknown name, a numeric signal, or a custom signal is a 400 — never a silent empty list. Example: `golden_cross`. |
| `transition` | query | string | no | `enter` (false→true) or `exit` (true→false). Optional everywhere — an ordinary filter. Same word `/v2/series` uses for the same primitive. Enum: `enter`, `exit`. Example: `enter`. |
| `merge_gap_seconds` | query | integer | no | Interval-union before the unpivot (TB-331, from the retiring spans route): runs of the signal separated by ≤ N seconds collapse into one, so a signal with thousands of per-tick fragments yields one `enter` and one `exit` per real run — `3600` for hourly signals, `86400` for daily. Default `0` = raw fragments. Requires `signal` AND an explicit `tickers` list (up to 50), filter mode only (no `q` / `join` / `group_by`): runs are computed over the signal's whole history per ticker, then the window and cursor apply to the edges. A cursor pins it; resend it unchanged when paging. Example: `86400`. |
| `ticker` | query | string | no | Single symbol. Beats `tickers` when both are passed. Example: `AAPL`. |
| `tickers` | query | string[] | no | Comma-separated symbols, up to 50. Not combinable with `universe`; `ticker` wins when both are passed. Example: `AAPL,MSFT`. |
| `universe` | query | string | no | System or caller-owned universe slug. Example: `top_100`. |
| `from` | query | string | no | `YYYY-MM-DD` or ISO timestamp (inclusive). `since` is accepted as an alias. Example: `2026-09-14`. |
| `to` | query | string | no | `YYYY-MM-DD` or ISO timestamp — a bare `YYYY-MM-DD` means through the end of that day; timestamps are exclusive. `until` is accepted as an alias. Example: `2026-06-28`. |
| `q` | query | string | no | SQL filter. This kind's payload fields are first-class typed columns here: `signal`, `transition` (text), `price`, `definition_version` (numeric) — plus ticker-state signals with `join=state` (a state column shadows a same-named payload field there: spell `payload->>'price'` if you mean the firing price under the join). Requires `signal=` on this kind (the named signal keeps the query on an index). Example: `transition = 'enter'`. |
| `join` | query | string | no | `state` widens `q` to ticker-state signals evaluated as of each firing's timestamp. Requires `signal=`. Enum: `state`. |
| `interval` | query | string | no | Grain the per-event state is reconstructed at when `join=state`: `1m`, `1h`, `1d`, or `auto` (default → `1d`). Enum: `1m`, `1h`, `1d`, `auto`. Default: `auto`. |
| `limit` | query | integer | no | Rows per page. Max 1000 — raise it and filter to one signal client-side. Default: `50`. Example: `5`. |
| `cursor` | query | string | no | Opaque cursor from the previous response. |

## Returns

- `as_of` (string) — Server time this response was assembled (ISO 8601).
- `query` (object) — Your filters, echoed.
- `count` (number) — Rows in this page.
- `next_cursor` (string) — Opaque token for the next page; `null` on the last page. Pass it back as `cursor`.
- `results` (array) — One row per event, newest first.
  - `ticker` (string) — Symbol that flipped.
  - `ts` (string) — The edge's timestamp (minute-grained).
  - `kind` (string) — Always `signal` on this view.
  - `payload` (object) — The flip.
    - `signal` (string) — The built-in boolean that flipped.
    - `transition` (string) — `enter` (false→true) or `exit` (true→false). **Renamed from `direction` on 2026-08-05** to match `/v2/series`, which has always called this a transition.
    - `price` (number, optional) — Price at the edge.
    - `definition_version` (number, optional) — Definition version of the boolean that produced the edge — an integer (`6` is current-canonical; `1`/`3`/`4`/`5` are legacy).

## Status codes

- **200** — Standard events envelope.
- **400** — `not_supported_for_kind` — `q` or `join=state` on `kind=signal` without `signal=`, or `group_by` on `kind=signal` at all. Also `not_a_flag_signal` / `not_supported_for_custom_signal` when `signal=` names a numeric signal or a custom signal.
- **503** — `kind_unavailable` — the signal event log is temporarily unavailable; retry later. 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-07-30T18:20:11.000Z",
  "query": { "kind": "signal", "ticker": "AAPL", "limit": 20 },
  "count": 2,
  "next_cursor": null,
  "results": [
    { "ticker": "AAPL", "ts": "2026-07-29T23:29:33.000Z", "kind": "signal",
      "payload": { "signal": "range_break_15m_up", "transition": "enter",
                   "price": 339.969, "definition_version": 6 } },
    { "ticker": "AAPL", "ts": "2026-07-29T23:20:43.000Z", "kind": "signal",
      "payload": { "signal": "volume_burst_5x", "transition": "exit",
                   "price": 339.425, "definition_version": 6 } }
  ]
}
```

## More examples

### One signal, across the whole market

Request:

```shell
curl "https://api.tickerbot.io/v2/events?kind=signal&signal=golden_cross&transition=enter&from=2026-08-17&limit=50" \
  -H "Authorization: Bearer YOUR_KEY"
```

Response (`200`):

```json
{
  "as_of": "2026-08-12T15:07:53.228Z",
  "query": { "kind": "signal", "signal": "golden_cross", "transition": "enter",
             "since": "2026-07-01T00:00:00.000Z", "limit": 50 },
  "count": 2,
  "next_cursor": "eyJ2IjoxLCJwb3MiOnsic2ln…",
  "results": [
    { "ticker": "NVDA", "ts": "2026-07-22T13:31:00.000Z", "kind": "signal",
      "payload": { "signal": "golden_cross", "transition": "enter",
                   "price": 181.4, "definition_version": 1 } },
    { "ticker": "KO", "ts": "2026-07-18T14:02:00.000Z", "kind": "signal",
      "payload": { "signal": "golden_cross", "transition": "enter",
                   "price": 69.12, "definition_version": 1 } }
  ]
}
```

### Cross-reference against state at the moment it fired

Request:

```shell
curl -G "https://api.tickerbot.io/v2/events" \
  --data-urlencode "kind=signal" \
  --data-urlencode "signal=gap_up" \
  --data-urlencode "transition=enter" \
  --data-urlencode "join=state" \
  --data-urlencode "q=above_sma_200 = true" \
  --data-urlencode "from=2026-08-17" \
  -H "Authorization: Bearer YOUR_KEY"
```

Response (`200`):

```json
{
  "as_of": "2026-08-12T15:07:53.228Z",
  "query": { "kind": "signal", "signal": "gap_up", "transition": "enter",
             "q": "above_sma_200 = true", "join": "state", "state_resolution": "1d" },
  "_meta": { "state_interval": "1d" },
  "count": 1,
  "next_cursor": null,
  "results": [
    { "ticker": "MSFT", "ts": "2026-07-24T13:30:00.000Z", "kind": "signal",
      "payload": { "signal": "gap_up", "transition": "enter",
                   "price": 511.7, "definition_version": 1 } }
  ]
}
```

## Notes

- This is [`GET /v2/events`](/docs/endpoints/events/query) with `kind=signal`; it has its own page because the payload is its own contract. Firings are computed by the same shared rule that fires webhooks, so the log and the push cannot disagree. Latency ≈ 1 minute (written by the minute pipeline).
- **Name the signal to use the grammar.** Filter mode needs nothing extra — scope with `kind` / `ticker` / `tickers` / `universe` / `from` / `to` / `limit` and page. `q` and `join=state` additionally require `signal=<name>`: the firing log is ~175M rows, and naming the signal is what lets it be read from an index instead of scanned, so every `golden_cross` enter across the whole market for a week comes back in milliseconds. Without it the request is refused with a 400 naming the param rather than timing out.
- `group_by` is not available on this kind. An aggregate has no `limit` to stop at, so it reads every firing in the window and the cost tracks how often the signal fires; naming a `signal=` does not rescue it. A mixed `kind` list containing `signal` is refused for the same reason — drop `signal` from `kind` to aggregate the rest.
- One fact, two shapes: a flip as a point-event is this kind; flips inline on a value series is `transitions_only` on [State over time](/docs/endpoints/series/state). The on/off-interval shape, [Signal spans](/docs/endpoints/signals/events), is deprecated (sunset 2026-10-31); `merge_gap_seconds` here yields one `enter` and one `exit` per run instead.
- Built-in booleans only — a forward-only boolean has no firings before its first computed day (see [coverage](/docs/endpoints/tickers/coverage)). Numeric-threshold flips are an as-of question, not a stored event.
- For pushes on flips, use a [signal-trigger webhook](/docs/endpoints/webhooks/create) — signal webhooks ride the sub-minute state path, not event ingest.

---

Interactive sandbox + parameter editor: https://tickerbot.io/docs/endpoints/events/signal-firings
