# POST /v2/scan/explain

**Explain a scan**

Takes the same parameters as `/v2/scan` (row or aggregate mode, live or `asof`) and reports what WOULD happen: whether the query validates, the predicate after custom-signal expansion and column quoting (`compiled_q` — what actually runs), the resolved universe scope and asof tier, and the Postgres planner's cost/row estimate. Nothing executes.

Always responds **200** — failures come back as `{ valid: false, stage, error, message }` where `stage` names where it died (`inputs`, `custom_signals`, `scope`, `compile`, `plan`), so an editor or CI check can diagnose without special-casing status codes. GET with query params also works.

## Plan access

- **Plan access.** Included on every plan, Free through Enterprise.
- **Rate limit.** Hobby 600/min · Pro 6,000/min · Scale 60,000/min.
- **Universe.** All 13,433+ tracked tickers on every plan.

## Body parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `q` | body | string | yes | The scan predicate to explain. |
| `universe` | body | string | no | Universe scope to resolve. |
| `asof` | body | string | no | Explain the point-in-time form — resolves and reports the serving tier (`resolution`). The asof plan window applies here too. |
| `group_by` | body | string | no | Explain aggregate mode (with `select`/`having`). |
| `order` | body | string | no | Sort column — validated like the real scan. |
| `fields` | body | string | no | Extra columns — validated like the real scan. |

## Status codes

- **200** — `{ valid, mode, query, compiled_q, custom_signals_expanded, resolution?, estimate? }` — or `{ valid: false, stage, error, message }`.

## Sample response

```json
{
  "as_of": "2026-07-27T17:20:00.000Z",
  "valid": true,
  "mode": "rows",
  "query": { "q": "my_dip AND market_cap > 1e9", "order": "day_change_pct", "fields": [], "full": false, "dir": "desc", "universe": null },
  "compiled_q": "((rsi_14 < 30 AND above_sma_200)) AND market_cap > 1e9",
  "custom_signals_expanded": true,
  "estimate": { "total_cost": 1873.2, "estimated_rows": 214 }
}
```

## Examples

### Validate before subscribing

Request:

```shell
curl -X POST "https://api.tickerbot.io/v2/scan/explain" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"my_dip AND market_cap > 1e9"}'
```

Response (`200`):

```json
{
  "as_of": "2026-07-27T17:20:00.000Z",
  "valid": true,
  "mode": "rows",
  "query": { "q": "my_dip AND market_cap > 1e9", "order": "day_change_pct", "fields": [], "full": false, "dir": "desc", "universe": null },
  "compiled_q": "((rsi_14 < 30 AND above_sma_200)) AND market_cap > 1e9",
  "custom_signals_expanded": true,
  "estimate": { "total_cost": 1873.2, "estimated_rows": 214 }
}
```

## Notes

- The same validation runs implicitly when you subscribe a query — explain lets you see the result without creating anything.

---

Interactive sandbox + parameter editor: https://tickerbot.io/docs/endpoints/scan/explain
