Hawksearch Analytics API
A REST API that gives you direct access to your Hawksearch analytics data — search events, clicks, sales, and recommendation interactions. Build custom dashboards, feed data into your BI tools, or power site-level reporting with a single integration.
Endpoints
API Keys
Every request must include your API key in the x-api-key header. Your key is scoped to your account — it automatically restricts results to your site's data only.
POST / HTTP/1.1
Host: api.analytics.hawksearch.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Base URL
https://api.analytics.hawksearch.com
All endpoints are relative to this base URL. The API is HTTPS-only.
Data Model
All data lives in a single summary table. Each row has an event_type that determines which fields are populated. Always filter on event_type when querying fields specific to one row type.
| event_type | Represents | Key fields |
|---|---|---|
| search | A search request. Includes click details if the search led to a click. | keyword, no_of_results, visit flags |
| cart | An item added to cart, linked to the originating search. | item_id, item_price, item_quantity |
| sale | A purchased item, linked to the originating search. | item_total, order_number, order_total |
| rec_click | A click on a recommended item. Carries sale fields when the click led to a purchase. | widget_id, widget_name, item_total |
search rows (had_click, had_cart, had_sale, had_rec_click) reflect what happened across the entire session — not just the individual search row.
Field Reference
Use GET / to retrieve the live schema. The tables below document the most commonly used fields.
Search fields
| Field | Type | Description |
|---|---|---|
event_date | date | Date of the search |
keyword | string | Search term entered by the user |
tracking_id | string | Unique ID for a single search request |
visit_id | string | Session ID — shared across searches in one visit |
visitor_id | string | Browser/device identifier |
no_of_results | number | Results returned for this search |
engine_id / engine_name | string | Engine identifier and display name |
event_year / event_month | number | Partition keys — include in filters for best performance |
keyword is blank for landing page/category browsing, not just keyword searches. Whenever keyword is used as a dimension, the API automatically excludes blank-keyword rows from the result — the same guard every keyword-grained pre-built report (/queries, /sales, etc.) already applies at the source. This only affects grouping; filtering on keyword directly (e.g. contains/equals) is unaffected.
Visit-level flags (on search rows)
| Field | Description |
|---|---|
had_click | This search had at least one click |
had_cart | This visit included an add-to-cart |
had_sale | This visit included a purchase |
had_rec_impression | This visit had a recommendation impression |
had_rec_click | This visit had a recommendation click |
Bot classification (on search rows)
| Field | Type | Description |
|---|---|---|
is_bot | boolean | Bot/crawler classification of the request's User-Agent. null means unknown (historical data, or no captured User-Agent) — not the same as false |
bot_name | string | Recognized crawler name (e.g. Googlebot, Bingbot), null if not a recognized bot |
is_bot notEquals true rather than is_bot equals false, since the latter silently excludes unknown (null) rows too.
Item fields (cart, sale, rec_click rows)
| Field | Type | Description |
|---|---|---|
item_id / item_name | string | Item identifier and title |
item_quantity | number | Quantity |
item_price | number | Unit price |
item_total | number | Line total. Populated on sale rows and attributed rec_click rows. |
order_number | string | Order identifier |
order_total | number | Total order value |
order_tax | number | Order tax amount — same population rule as order_total |
order_sub_total | number | Order subtotal (pre-tax) — same population rule as order_total |
currency | string | Order currency code (e.g. USD) — same population rule as order_total. Expect one constant value per client; more than one distinct value indicates a data-quality issue, not real multi-currency activity |
Recommendation fields (rec_click rows)
| Field | Type | Description |
|---|---|---|
widget_id | string | Recommendation widget identifier |
widget_name | string | Recommendation widget display name |
Facet fields
Facet fields aren't part of the base summary table — referencing either one automatically pulls in facet selection data for the matching search, so they can be freely combined with any other dimension, measure, or filter (including sale/cart fields).
| Field | Type | Description |
|---|---|---|
facet_field | string | Facet field selected on the search (e.g. brandid, categories) |
facet_name | string | Facet display name, resolved from vwFacet (NULL if unresolved) |
facet_value | string | Value selected for the facet (e.g. clorox) |
facet_field/facet_value are used — prefer count_distinct(tracking_id) or count_distinct(order_number) over raw sum/count when combining facet fields with sale or cart measures, to avoid double-counting.
Sorting fields
Sorting fields live directly on summary — one value per search, no join required.
| Field | Type | Description |
|---|---|---|
sort_by | string | Raw sort expression sent to the engine |
sorting_set_id | number | Sorting Set selected on the search, 0/NULL = default |
sorting_set_field_id | number | Sorting Set Field selected on the search, 0/NULL = default |
sorting_set_name | string | Sorting Set display name, resolved from vwSortingSet (NULL if unresolved) |
sorting_set_field_name | string | Sorting Set Field display label, resolved from vwSortingSetField (NULL if unresolved) |
Pagination fields
Pagination fields live directly on summary — one value per search, no join required.
| Field | Type | Description |
|---|---|---|
pagination_set_id | number | Pagination Set selected on the search, 0/NULL = default |
pagination_set_option | number | Pagination Set option selected (e.g. page size), 0/NULL = default |
pagination_set_name | string | Pagination Set display name, resolved from vwPaginationSet (NULL if unresolved) |
pagination_set_option_label | string | Pagination option display label (e.g. "24 Items Per Page"), resolved from vwPaginationSet's Options JSON (NULL if unresolved) |
page_no | number | Result page number the visitor viewed ("Current Page"), 1-indexed, defaults to 1 — independent of pagination_set_option |
Ad-hoc Queries
Send a POST / with a structured query to pull any combination of fields from your data. Specify dimensions (what to group by), measures (what to aggregate), and filters (what to include).
dimensions / measures / filters pattern will feel immediately familiar. This is standard practice in the analytics engineering community for abstracting SQL behind a portable, declarative query layer.
{
"dimensions": ["keyword"],
"measures": [
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_quantity","fn": "sum", "alias": "units_sold" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_month"] }
],
"order": [{ "field": "revenue", "dir": "desc" }],
"limit": 25
}
Request fields
| Field | Type | Default | Description |
|---|---|---|---|
query | string | — | Natural language question. When set, structured fields are ignored. |
dimensions | string[] | — | Fields to select and group by |
measures | object[] | — | Aggregations to compute |
filters | object[] | — | Row-level conditions (WHERE). ANDed together. |
having | object[] | — | Post-aggregation conditions (HAVING). Member must be a measure alias. |
order | object[] | — | Sort order |
page | number | 1 | Page number (1-based) |
limit | number | 100 | Rows per page (max 1000) |
dryRun | boolean | false | Return generated SQL without executing |
queryExecutionId | string | — | Re-fetch a prior cached result (valid 24 h) |
Natural Language
Set query to a plain English question instead of building the structured fields manually. The API interprets it and runs the appropriate query.
{ "query": "Top 10 keywords by revenue last month" }
Filters
Each filter object has member (the field), operator, and values. Multiple filters are combined with AND.
Date range shortcuts
// Shortcut
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
// Explicit range
{ "member": "event_date", "operator": "inDateRange", "values": ["2026-06-01", "2026-06-30"] }
// Null check
{ "member": "widget_id", "operator": "set" }
Measures
Each measure object defines one aggregation. When measures are present, dimensions automatically become the GROUP BY.
| Property | Required | Description |
|---|---|---|
field | Yes | Field to aggregate. Use "*" with count for a row count. |
fn | Yes | count, count_distinct, sum, avg, min, max |
alias | No | Column name in the response. Auto-generated if omitted. |
[
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "*", "fn": "count", "alias": "row_count" }
]
Pre-built Reports
Report endpoints run fixed, pre-optimised queries — faster than ad-hoc queries and require no field knowledge. Send a date range and receive paginated results. Each endpoint also accepts its own filters (pre-aggregation, on that endpoint's raw table columns) and having (post-aggregation, on that endpoint's own output measures) — same shape as the ad-hoc engine's, but scoped per endpoint rather than to a single shared schema. Submit an unrecognized member to get a 400 response listing exactly which fields that endpoint supports.
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"page": 1,
"limit": 100
}
| Endpoint | What it returns |
|---|---|
/searches | Search count by engine and group, per day |
/users | Unique visitors/visits, average results, converting visitors, and revenue, per day |
/queries | Row-level keyword performance per day (keyword, date, request type, engine) — for client-side filtering/grouping; multiple rows per date expected |
/click-positions | Sitewide click-position distribution (rank 1 / 2-3 / 4-10 / 11+), not keyword-grained |
/poor-results | Keywords returning 10 or fewer results |
/spelling | Spelling correction counts, per day |
/sales | Revenue and orders by product and keyword |
/recommendations | Widget impressions, clicks, add-to-carts, orders, and revenue per day |
/facets | Search volume, clicks, carts, sales, and revenue by facet field, facet value, and keyword |
/landing-page-facets | Facet search volume for searches on a landing page |
/sorting | Search volume by sorting option selected, per day |
/pagination | Search volume by pagination option selected, per day |
/campaigns | Campaign (banner) impressions, clicks, and CTR, per day |
/campaigns-summary | Campaign (banner) impressions, clicks, and CTR summarized across the date range |
/banners | Individual banner (content item) impressions, clicks, and CTR, per day |
/banners-summary | Individual banner (content item) impressions, clicks, and CTR summarized across the date range |
/autocomplete-clicks | Autocomplete suggestion clicks by keyword/suggestion, summarized across the date range |
/autocomplete-trending-categories | Autocomplete trending-category suggestion clicks, summarized across the date range |
/autocomplete-trending-items | Autocomplete trending-item suggestion clicks, summarized across the date range |
/product-ratings | Product ratings (count and average) summarized across the date range |
/device-browser | Search/conversion breakdown by browser, operating system, and device category (mobile/tablet/desktop), per day |
/landing-pages | Landing page overview, per day — searches, visits, clicks, average results, carts, orders, revenue |
/geo-distribution | Search/conversion breakdown by geography (country/region/state/city), per day |
/redirects | Redirect Rule firing counts, enriched with triggering keyword/landing page and sale attribution |
/product-impressions | Per-item impression/click/order/revenue from Top10List — surfaces impressed-but-not-clicked/bought items |
/search-funnel | Search → click → cart → order funnel counts per day, with CTR/cart-rate/order-rate computed at query time |
/page-loads | Page-load counts by page type and page path, per day, with session-attributed search/order activity |
Pagination
All responses include totalRecords, hasMore, page, and limit. Increment page while hasMore is true to fetch subsequent pages.
Each response also includes a queryExecutionId. Pass it back in a follow-up request to re-fetch the same result without re-running the query — results are cached for 24 hours.
{
"queryExecutionId": "a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"page": 1,
"limit": 25
}
page and limit must match the original request — the cached result contains only the rows for that page.
Dry Run
Add "dryRun": true to preview the generated SQL without executing it. The response will contain a sql field and no data. Works with structured queries and natural language queries alike — for a natural language query, this is the easiest way to see exactly what Bedrock translated your question into before it runs.
{
"dimensions": ["keyword"],
"measures": [{ "field": "item_total", "fn": "sum", "alias": "revenue" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "event_year", "operator": "equals", "values": ["2026"] }
],
"dryRun": true
}
{
"query": "Top 10 keywords by revenue this year",
"dryRun": true
}
Performance Tips
- Always include
event_yearand/orevent_monthin filters when targeting a specific period. These are partition keys and dramatically reduce data scanned. - Use date shortcuts (
last_30_days,last_month, etc.) for relative windows — they resolve server-side and keep your query always current. - Prefer pre-built report endpoints for common metrics — they query pre-aggregated tables and are faster than raw
summaryqueries. - Set
limitto the smallest value you need. UsehasMoreandtotalRecordsto decide whether to page further. - Re-use
queryExecutionIdfor retries or paginated UIs — avoids re-running expensive queries.
Response Format
All endpoints return the same JSON structure regardless of query type.
{
"columns": [
{ "name": "keyword", "type": "string" },
{ "name": "revenue", "type": "number" }
],
"data": [
{ "keyword": "running shoes", "revenue": "4521.50" },
{ "keyword": "blue jeans", "revenue": "3890.00" }
],
"totalRecords": 842,
"page": 1,
"limit": 25,
"hasMore": true,
"queryExecutionId": "a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
| Field | Description |
|---|---|
columns | Column names and inferred types (string, number, date) |
data | Array of rows. Each row is a flat key-value map; all values are strings. Null fields are omitted. |
totalRecords | Total matching rows across all pages |
hasMore | true if additional pages are available |
queryExecutionId | Pass back to re-fetch this result without re-running the query |
sql | Generated SQL. Only present when dryRun: true |
Errors
| Status | Meaning |
|---|---|
400 | Invalid request — unknown field, unsupported operator, or missing dimensions/measures |
401 | API key missing or invalid |
404 | Account not configured for analytics |
429 | Rate limit exceeded |
500 | Query execution failed |
502 | Natural language translation failed — rephrase or use structured mode |
{
"error": "Bad Request",
"message": "Unknown dimension: 'sale_item_id'."
}
Sample Queries
Ad-hoc Queries
Click-through rate by keyword
{
"dimensions": ["keyword"],
"measures": [
{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" },
{ "field": "click_item_id", "fn": "count_distinct", "alias": "clicks" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }],
"limit": 50
}
Cart abandonment — added to cart but did not purchase
{
"dimensions": ["keyword"],
"measures": [{ "field": "visit_id", "fn": "count_distinct", "alias": "abandoned_visits" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "had_cart", "operator": "equals", "values": ["true"] },
{ "member": "had_sale", "operator": "equals", "values": ["false"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_month"] }
],
"order": [{ "field": "abandoned_visits", "dir": "desc" }]
}
Widget revenue (recommendation-attributed)
{
"dimensions": ["widget_id", "widget_name"],
"measures": [
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_quantity", "fn": "sum", "alias": "units_sold" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["rec_click"] },
{ "member": "item_total", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
Revenue by banner
{
"dimensions": ["banner_id", "banner_name"],
"measures": [
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "order_total", "fn": "sum", "alias": "revenue" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["banner_click"] },
{ "member": "order_number", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
Attribution is visit-level, not item-level — a banner click has no associated item, so order_number/order_total reflect any order placed in the same visit as the click. Use count_distinct(order_number) rather than a raw row count/sum if the same banner may have been clicked more than once in a visit.
Revenue for a specific campaign
{
"dimensions": ["campaign_name"],
"measures": [
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "order_total", "fn": "sum", "alias": "revenue" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["banner_click"] },
{ "member": "campaign_name", "operator": "equals", "values": ["Summer Sale"] },
{ "member": "order_number", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
Revenue from autocomplete trending-item clicks
{
"dimensions": ["item_id", "item_name"],
"measures": [
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["autocomplete_trending_item_click"] },
{ "member": "item_total", "operator": "set" }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
Attribution is item-level here, same as rec_click — order_number/order_total only populate when the exact clicked item was purchased in the same visit, unlike banner_click's visit-level attribution above.
Top recommended items by widget
{
"dimensions": ["widget_id", "widget_name", "item_id", "item_name"],
"measures": [
{ "field": "*", "fn": "count", "alias": "clicks" },
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["rec_click"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "clicks", "dir": "desc" }],
"limit": 50
}
Monthly revenue trend
{
"dimensions": ["event_year", "event_month"],
"measures": [
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_quantity", "fn": "sum", "alias": "units_sold" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "event_year", "operator": "equals", "values": ["2026"] }
],
"order": [{ "field": "event_month", "dir": "asc" }]
}
High-revenue keywords — using having
{
"dimensions": ["keyword"],
"measures": [
{ "field": "item_total", "fn": "sum", "alias": "revenue" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_month"] }
],
"having": [
{ "member": "revenue", "operator": "gte", "values": ["1000"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
Search volume by facet
{
"dimensions": ["facet_field", "facet_value"],
"measures": [
{ "field": "tracking_id", "fn": "count_distinct", "alias": "search_count" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "event_year", "operator": "equals", "values": ["2026"] }
],
"order": [{ "field": "search_count", "dir": "desc" }]
}
One facet, broken down by keyword
{
"dimensions": ["facet_value", "keyword"],
"measures": [
{ "field": "tracking_id", "fn": "count_distinct", "alias": "search_count" }
],
"filters": [
{ "member": "facet_field", "operator": "equals", "values": ["brandid"] },
{ "member": "event_type", "operator": "equals", "values": ["search"] }
],
"order": [{ "field": "search_count", "dir": "desc" }]
}
Revenue for searches with a specific facet selected
{
"dimensions": ["facet_value"],
"measures": [
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_total", "fn": "sum", "alias": "revenue" }
],
"filters": [
{ "member": "facet_field", "operator": "equals", "values": ["brandid"] },
{ "member": "facet_value", "operator": "equals", "values": ["clorox"] },
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "event_year", "operator": "equals", "values": ["2026"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
True zero-result keywords
{
"dimensions": ["keyword"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "no_of_results", "operator": "equals", "values": ["0"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }],
"limit": 50
}
A stronger signal than /poor-results' no_of_results <= 10 threshold — this isolates keywords that returned nothing at all, a pure catalog-gap indicator.
Custom sort options and their keywords
{
"dimensions": ["sorting_set_name", "sorting_set_field_name", "keyword"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "sorting_set_id", "operator": "gt", "values": ["0"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }],
"limit": 50
}
sorting_set_id > 0 excludes the default/no-sort case — same "explicit sort applied" definition /sorting uses.
Deep pagination — visitors digging past the first couple of pages
{
"dimensions": ["keyword", "page_no"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "page_no", "operator": "gte", "values": ["3"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }],
"limit": 50
}
Elevated searches reaching page_no >= 3 for a given keyword is a proxy for poor default ranking — visitors are digging past the top results to find what they want.
Landing page vs. sitewide search volume
// Landing page activity
{
"dimensions": ["landing_page_url", "page_name"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "landing_page_url", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }]
}
// Sitewide (non-landing-page) activity — flip set to notSet
{
"dimensions": ["keyword"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "landing_page_url", "operator": "notSet" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }]
}
Revenue from spelling-corrected searches
{
"dimensions": ["original_keyword", "corrected_keyword"],
"measures": [
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_total", "fn": "sum", "alias": "revenue" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "original_keyword", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
original_keyword/corrected_keyword are carried onto every row type for a session, not just its search row, so filtering event_type = "sale" here still isolates orders from sessions where the corrector fired — validates whether it's actually recovering lost intent.
Keyword text search
{
"dimensions": ["keyword"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "keyword", "operator": "contains", "values": ["running"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }]
}
Search volume by request type
{
"dimensions": ["request_type"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "request_type", "operator": "in", "values": ["DefaultSearch", "ConceptSearch", "UnifiedSearch"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }]
}
Refinement/non-default search requests
{
"dimensions": ["request_type"],
"measures": [{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" }],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "request_type", "operator": "notEquals", "values": ["DefaultSearch"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }]
}
Revenue by Visitor Target segment
{
"dimensions": ["visitor_target_name"],
"measures": [
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_total", "fn": "sum", "alias": "revenue" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "revenue", "dir": "desc" }]
}
Referencing visitor_target_name JOINs summary against visitortargetselections — only sessions that matched at least one Visitor Target segment appear, one row per matched segment. Unlike the pre-built datasets' visitor_target_id = 0 "All Visitor Targets" anchor row, there's no equivalent here — omit this dimension entirely to get the true unsegmented total instead of filtering to a specific value.
Facet performance within a Visitor Target segment
{
"dimensions": ["facet_value", "visitor_target_name"],
"measures": [
{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" },
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" }
],
"filters": [
{ "member": "facet_field", "operator": "equals", "values": ["brandid"] },
{ "member": "visitor_target_name", "operator": "equals", "values": ["Returning Visitors"] },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "searches", "dir": "desc" }]
}
Combining a facet field with visitor_target_name joins two independent one-to-many bridge tables off tracking_id — a search matching N facets and M Visitor Targets produces N×M rows. Use count_distinct(tracking_id)/count_distinct(order_number), never a raw row count or sum, when combining the two.
Where clicked results rank on the page
{
"dimensions": ["keyword"],
"measures": [
{ "field": "click_item_id", "fn": "count_distinct", "alias": "clicks" },
{ "field": "click_element_position", "fn": "avg", "alias": "avg_click_position" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["search"] },
{ "member": "click_item_id", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"order": [{ "field": "clicks", "dir": "desc" }],
"limit": 50
}
Revenue from logged-in visitors
{
"measures": [
{ "field": "order_number", "fn": "count_distinct", "alias": "orders" },
{ "field": "item_total", "fn": "sum", "alias": "revenue" }
],
"filters": [
{ "member": "event_type", "operator": "equals", "values": ["sale"] },
{ "member": "user_id", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
]
}
user_id only populates for sessions that logged in (resolved from EventLogin); flip to notSet for the anonymous-visitor equivalent.
Landing page lost opportunity
{
"dimensions": ["landing_page_url", "page_name"],
"measures": [
{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" },
{ "field": "no_of_results", "fn": "avg", "alias": "avg_no_of_results" }
],
"filters": [
{ "member": "landing_page_url", "operator": "set" },
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"having": [
{ "member": "searches", "operator": "gte", "values": ["50"] },
{ "member": "avg_no_of_results", "operator": "lte", "values": ["5"] }
],
"order": [{ "field": "searches", "dir": "desc" }],
"limit": 50
}
Surfaces landing pages driving real traffic into a catalog gap — high search volume, poor average results. Because this queries summary directly (one row per search event), a plain avg measure is already correct here — no weighting needed. Contrast with the pre-aggregated /landing-pages report endpoint below, whose avg_no_of_results column is itself a daily average and needs a search_count-weighted re-average when rolling multiple days together (see that endpoint's description). Tune the having thresholds (minimum searches, maximum avg results) to fit your catalog size.
Keyword lost opportunity
{
"dimensions": ["keyword"],
"measures": [
{ "field": "tracking_id", "fn": "count_distinct", "alias": "searches" },
{ "field": "no_of_results", "fn": "avg", "alias": "avg_no_of_results" },
{ "field": "click_item_id", "fn": "count_distinct", "alias": "clicks" }
],
"filters": [
{ "member": "event_date", "operator": "inDateRange", "values": ["last_30_days"] }
],
"having": [
{ "member": "searches", "operator": "gte", "values": ["50"] },
{ "member": "avg_no_of_results", "operator": "lte", "values": ["5"] }
],
"order": [{ "field": "searches", "dir": "desc" }],
"limit": 50
}
The direct modern equivalent of the legacy "Searches with Poor Results"/"Searches without Clicks" reports — high search volume, few results, and (via the clicks measure) visibility into whether visitors clicked anything anyway despite the poor result count. This is more targeted than the pre-built /poor-results endpoint below, which has a fixed no_of_results <= 10 threshold and no minimum-volume filter — the having clause here lets you tune both independently. Same weighting note as landing pages doesn't apply — avg is already correct against row-level summary data.
Natural Language
Pass a plain English question using the query field. The API interprets it and runs the appropriate query automatically.
Search volume
{ "query": "How many searches did we have last month?" }
Top keywords by revenue
{ "query": "Top 10 keywords by revenue in June 2026" }
Poor-performing search terms
{ "query": "Which keywords returned no results last 30 days?" }
Recommendation performance
{ "query": "Which recommendation widgets drove the most revenue this month?" }
Sales trend
{ "query": "Show me monthly revenue for 2026" }
Revenue through a specific banner
{ "query": "What's the revenue for all sales through the Homepage Hero banner?" }
Campaign revenue this month
{ "query": "Which campaigns drove the most revenue this month?" }
Revenue from autocomplete trending-item clicks
{ "query": "Revenue from products clicked as autocomplete trending items" }
True zero-result keywords
{ "query": "Which keywords returned zero results in the last 30 days?" }
Custom sort usage
{ "query": "Which keywords are searched most with a non-default sort applied?" }
Deep pagination
{ "query": "Which keywords have visitors paging past page 3 of results?" }
Landing page search volume
{ "query": "How many searches happened on landing pages last month?" }
Spelling-correction value
{ "query": "How much revenue came from spelling-corrected searches last month?" }
Search volume by request type
{ "query": "Break down search volume by request type over the last 30 days" }
Refinement searches
{ "query": "How many searches used something other than the default search type last 30 days?" }
Revenue by Visitor Target segment
{ "query": "Show me revenue broken down by visitor segment for the last 30 days" }
Average click position
{ "query": "What's the average click position by keyword over the last 30 days?" }
Logged-in visitor revenue
{ "query": "How much revenue came from logged-in visitors last month?" }
Landing page lost opportunity
{ "query": "Which landing pages had at least 50 searches and an average of 5 or fewer results last 30 days?" }
Keyword lost opportunity
{ "query": "Which keywords had at least 50 searches and an average of 5 or fewer results last 30 days?" }
Pre-built Reports
Search volume by day
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Unique visitors and conversion, per day
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Row-level keyword performance per day
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 500
}
Sitewide click-position distribution
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Keywords returning few results
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 50
}
Spelling correction activity
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Revenue by product and keyword
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 100
}
Recommendation widget performance
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 50
}
Facet lost opportunity
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"having": [
{ "member": "search_count", "operator": "gte", "values": ["20"] },
{ "member": "click_count", "operator": "lte", "values": ["1"] }
],
"limit": 50
}
Same shape as the product/keyword/landing-page lost-opportunity queries — a facet value selected often (search_count >= 20) that almost never leads to a click is a candidate for review: bad facet configuration, mislabeled inventory, or a genuine catalog gap for that filter combination. Add { "member": "sale_count", "operator": "equals", "values": ["0"] } to having for a stricter "never converts" cut, or a facet_field filter to scope the report to one facet (e.g. just brandid) instead of every facet on the site.
Campaign performance
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 50
}
Individual banner performance
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 50
}
Product ratings
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 50
}
Search/conversion by browser and OS
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Landing page overview
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"limit": 50
}
Search/conversion by geography
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Redirect Rule firing counts
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Per-item search-result impressions / product lost opportunity
{
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"having": [
{ "member": "impression_count", "operator": "gte", "values": ["50"] },
{ "member": "click_count", "operator": "lte", "values": ["1"] }
],
"limit": 200
}
Rows come back sorted by impression_count descending. The having clause above isolates product-level lost opportunity directly — items Hawksearch renders often (impression_count >= 50) but visitors almost never click (click_count <= 1) — no client-side post-processing needed. This still can't be expressed as an ad-hoc/natural-language POST / query, though — impression data comes from Top10List, which only exists on productimpressions, not summary (the only table that engine queries), so /product-impressions is the only way to reach it. Every fixed report endpoint accepts its own filters/having fields the same way — see each endpoint's own description above for its filterable/having-able field list, or submit an unknown member to get a 400 response listing the valid ones.
Tuning this query:
- For a stricter cut ("never clicked at all," not just rarely), use
{ "member": "click_count", "operator": "equals", "values": ["0"] }instead oflte 1. - Add
order_count/revenuetohavingthe same way to find items that are impressed and even clicked, but never actually purchased. - Add a
filtersentry (e.g.{ "member": "keyword", "operator": "equals", "values": ["running shoes"] }) to scope the whole report to one keyword instead of the entire catalog. - Add
"dryRun": trueto see the generated SQL without spending an Athena query while you tune thresholds — same convention as the ad-hocPOST /endpoint. - Thresholds don't auto-scale with the date range —
impression_count/click_countare summed across the wholestartDate–endDatewindow, so a 50-impression cutoff that's meaningful over 30 days will over-match on a 1-day request and under-match on a 1-year one. Re-tune the threshold (or normalize it yourself, e.g. impressions ÷ days) when changing the range.
Search-to-order conversion funnel
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}
Page-load counts by page type and path
{
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}