Hawksearch Analytics Developer Portal
Overview

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.

Interactive reference — Use the API Reference to explore every endpoint and try requests directly from your browser.

Endpoints

GET / Full field schema — types, operators, valid values
POST / Ad-hoc query — define dimensions, measures, and filters
POST /searches Search volume by engine, per day
POST /users Unique visitors/visits, average results, converting visitors, and revenue, per day
POST /queries Row-level keyword performance per day, for client-side filtering/grouping (expect multiple rows per date)
POST /click-positions Sitewide click-position distribution (rank 1 / 2-3 / 4-10 / 11+), not keyword-grained
POST /poor-results Keywords returning few results
POST /spelling Spelling correction activity, per day
POST /sales Revenue broken down by product and keyword
POST /recommendations Widget impressions, clicks, add-to-carts, orders, and revenue per day
POST /facets Search volume, clicks, carts, sales, and revenue by facet field, facet value, and keyword
POST /landing-page-facets Facet search volume for searches on a landing page
POST /sorting Search volume by sorting option selected, per day
POST /pagination Search volume by pagination option selected, per day
POST /campaigns Campaign (banner) impressions, clicks, and CTR, per day
POST /campaigns-summary Campaign (banner) impressions, clicks, and CTR summarized across the date range
POST /banners Individual banner (content item) impressions, clicks, and CTR, per day
POST /banners-summary Individual banner (content item) impressions, clicks, and CTR summarized across the date range
POST /autocomplete-clicks Autocomplete suggestion clicks by keyword/suggestion, summarized across the date range
POST /autocomplete-trending-categories Autocomplete trending-category suggestion clicks, summarized across the date range
POST /autocomplete-trending-items Autocomplete trending-item suggestion clicks, summarized across the date range
POST /product-ratings Product ratings (count and average) summarized across the date range
POST /device-browser Search/conversion breakdown by browser, operating system, and device category (mobile/tablet/desktop), per day
POST /landing-pages Landing page overview, per day — searches, visits, clicks, average results, carts, orders, revenue
POST /geo-distribution Search/conversion breakdown by geography (country/region/state/city), per day
POST /redirects Redirect Rule firing counts, enriched with triggering keyword/landing page and sale attribution
POST /product-impressions Per-item impression/click/order/revenue from Top10List — surfaces impressed-but-not-clicked/bought items
POST /search-funnel Search → click → cart → order funnel counts per day, with CTR/cart-rate/order-rate computed at query time
POST /page-loads Page-load counts by page type and page path, per day, with session-attributed search/order activity

Authentication

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.

HTTP
POST / HTTP/1.1
Host: api.analytics.hawksearch.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Contact your account manager to obtain an API key or to rotate an existing one. Keys cannot be created through the API.

Overview

Base URL

URL
https://api.analytics.hawksearch.com

All endpoints are relative to this base URL. The API is HTTPS-only.


Data

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
Visit-level flags on search rows (had_click, had_cart, had_sale, had_rec_click) reflect what happened across the entire session — not just the individual search row.
Data

Field Reference

Use GET / to retrieve the live schema. The tables below document the most commonly used fields.

Search fields

FieldTypeDescription
event_datedateDate of the search
keywordstringSearch term entered by the user
tracking_idstringUnique ID for a single search request
visit_idstringSession ID — shared across searches in one visit
visitor_idstringBrowser/device identifier
no_of_resultsnumberResults returned for this search
engine_id / engine_namestringEngine identifier and display name
event_year / event_monthnumberPartition 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)

FieldDescription
had_clickThis search had at least one click
had_cartThis visit included an add-to-cart
had_saleThis visit included a purchase
had_rec_impressionThis visit had a recommendation impression
had_rec_clickThis visit had a recommendation click

Bot classification (on search rows)

FieldTypeDescription
is_botbooleanBot/crawler classification of the request's User-Agent. null means unknown (historical data, or no captured User-Agent) — not the same as false
bot_namestringRecognized crawler name (e.g. Googlebot, Bingbot), null if not a recognized bot
Classification is a regex match against known bot/crawler User-Agent signatures — it only catches traffic that self-identifies as a crawler, not a UA deliberately spoofed as a normal browser. Filter human-only traffic with 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)

FieldTypeDescription
item_id / item_namestringItem identifier and title
item_quantitynumberQuantity
item_pricenumberUnit price
item_totalnumberLine total. Populated on sale rows and attributed rec_click rows.
order_numberstringOrder identifier
order_totalnumberTotal order value
order_taxnumberOrder tax amount — same population rule as order_total
order_sub_totalnumberOrder subtotal (pre-tax) — same population rule as order_total
currencystringOrder 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)

FieldTypeDescription
widget_idstringRecommendation widget identifier
widget_namestringRecommendation 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).

FieldTypeDescription
facet_fieldstringFacet field selected on the search (e.g. brandid, categories)
facet_namestringFacet display name, resolved from vwFacet (NULL if unresolved)
facet_valuestringValue selected for the facet (e.g. clorox)
A search with more than one facet selected produces one row per facet when 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.

FieldTypeDescription
sort_bystringRaw sort expression sent to the engine
sorting_set_idnumberSorting Set selected on the search, 0/NULL = default
sorting_set_field_idnumberSorting Set Field selected on the search, 0/NULL = default
sorting_set_namestringSorting Set display name, resolved from vwSortingSet (NULL if unresolved)
sorting_set_field_namestringSorting 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.

FieldTypeDescription
pagination_set_idnumberPagination Set selected on the search, 0/NULL = default
pagination_set_optionnumberPagination Set option selected (e.g. page size), 0/NULL = default
pagination_set_namestringPagination Set display name, resolved from vwPaginationSet (NULL if unresolved)
pagination_set_option_labelstringPagination option display label (e.g. "24 Items Per Page"), resolved from vwPaginationSet's Options JSON (NULL if unresolved)
page_nonumberResult page number the visitor viewed ("Current Page"), 1-indexed, defaults to 1 — independent of pagination_set_option

Querying

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).

Familiar query format — The ad-hoc query schema is inspired by Cube.js, a widely adopted open-source analytics framework. If you've worked with Cube.js, Cube Cloud, or any BI tool built on top of it (Metabase, Superset, etc.), the 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.
JSON — Revenue by keyword
{
  "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

FieldTypeDefaultDescription
querystringNatural language question. When set, structured fields are ignored.
dimensionsstring[]Fields to select and group by
measuresobject[]Aggregations to compute
filtersobject[]Row-level conditions (WHERE). ANDed together.
havingobject[]Post-aggregation conditions (HAVING). Member must be a measure alias.
orderobject[]Sort order
pagenumber1Page number (1-based)
limitnumber100Rows per page (max 1000)
dryRunbooleanfalseReturn generated SQL without executing
queryExecutionIdstringRe-fetch a prior cached result (valid 24 h)
Querying

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.

JSON
{ "query": "Top 10 keywords by revenue last month" }
If the natural language query fails (HTTP 502), rephrase the question or switch to structured mode. Structured queries are always more reliable for production integrations.
Querying

Filters

Each filter object has member (the field), operator, and values. Multiple filters are combined with AND.

equalsExact match
notEqualsNot equal (does not exclude NULLs)
gt / gteGreater than / or equal
lt / lteLess than / or equal
containsSubstring match
startsWithPrefix match
inMatch any value in list
setField is not null
notSetField is null
inDateRangeDate range or shortcut

Date range shortcuts

today this_week this_month last_month last_30_days this_year last_year
JSON — Date range examples
// 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" }
Querying

Measures

Each measure object defines one aggregation. When measures are present, dimensions automatically become the GROUP BY.

PropertyRequiredDescription
fieldYesField to aggregate. Use "*" with count for a row count.
fnYescount, count_distinct, sum, avg, min, max
aliasNoColumn name in the response. Auto-generated if omitted.
JSON
[
  { "field": "item_total",   "fn": "sum",            "alias": "revenue" },
  { "field": "order_number", "fn": "count_distinct", "alias": "orders" },
  { "field": "*",            "fn": "count",          "alias": "row_count" }
]
Querying

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.

Full schema reference — See Dataset Schemas for every dataset's grain, complete column list, and worked SQL examples — useful whether you're calling these endpoints directly or querying the underlying tables yourself.
JSON — Report request
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "page": 1,
  "limit": 100
}
EndpointWhat it returns
/searchesSearch count by engine and group, per day
/usersUnique visitors/visits, average results, converting visitors, and revenue, per day
/queriesRow-level keyword performance per day (keyword, date, request type, engine) — for client-side filtering/grouping; multiple rows per date expected
/click-positionsSitewide click-position distribution (rank 1 / 2-3 / 4-10 / 11+), not keyword-grained
/poor-resultsKeywords returning 10 or fewer results
/spellingSpelling correction counts, per day
/salesRevenue and orders by product and keyword
/recommendationsWidget impressions, clicks, add-to-carts, orders, and revenue per day
/facetsSearch volume, clicks, carts, sales, and revenue by facet field, facet value, and keyword
/landing-page-facetsFacet search volume for searches on a landing page
/sortingSearch volume by sorting option selected, per day
/paginationSearch volume by pagination option selected, per day
/campaignsCampaign (banner) impressions, clicks, and CTR, per day
/campaigns-summaryCampaign (banner) impressions, clicks, and CTR summarized across the date range
/bannersIndividual banner (content item) impressions, clicks, and CTR, per day
/banners-summaryIndividual banner (content item) impressions, clicks, and CTR summarized across the date range
/autocomplete-clicksAutocomplete suggestion clicks by keyword/suggestion, summarized across the date range
/autocomplete-trending-categoriesAutocomplete trending-category suggestion clicks, summarized across the date range
/autocomplete-trending-itemsAutocomplete trending-item suggestion clicks, summarized across the date range
/product-ratingsProduct ratings (count and average) summarized across the date range
/device-browserSearch/conversion breakdown by browser, operating system, and device category (mobile/tablet/desktop), per day
/landing-pagesLanding page overview, per day — searches, visits, clicks, average results, carts, orders, revenue
/geo-distributionSearch/conversion breakdown by geography (country/region/state/city), per day
/redirectsRedirect Rule firing counts, enriched with triggering keyword/landing page and sale attribution
/product-impressionsPer-item impression/click/order/revenue from Top10List — surfaces impressed-but-not-clicked/bought items
/search-funnelSearch → click → cart → order funnel counts per day, with CTR/cart-rate/order-rate computed at query time
/page-loadsPage-load counts by page type and page path, per day, with session-attributed search/order activity

Advanced

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.

JSON — Re-fetch cached result
{
  "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.
Advanced

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.

JSON — structured
{
  "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
}
JSON — natural language
{
  "query": "Top 10 keywords by revenue this year",
  "dryRun": true
}
Advanced

Performance Tips

  • Always include event_year and/or event_month in 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 summary queries.
  • Set limit to the smallest value you need. Use hasMore and totalRecords to decide whether to page further.
  • Re-use queryExecutionId for retries or paginated UIs — avoids re-running expensive queries.

Reference

Response Format

All endpoints return the same JSON structure regardless of query type.

JSON
{
  "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"
}
FieldDescription
columnsColumn names and inferred types (string, number, date)
dataArray of rows. Each row is a flat key-value map; all values are strings. Null fields are omitted.
totalRecordsTotal matching rows across all pages
hasMoretrue if additional pages are available
queryExecutionIdPass back to re-fetch this result without re-running the query
sqlGenerated SQL. Only present when dryRun: true
Reference

Errors

StatusMeaning
400Invalid request — unknown field, unsupported operator, or missing dimensions/measures
401API key missing or invalid
404Account not configured for analytics
429Rate limit exceeded
500Query execution failed
502Natural language translation failed — rephrase or use structured mode
JSON — Error response
{
  "error": "Bad Request",
  "message": "Unknown dimension: 'sale_item_id'."
}
Reference

Sample Queries

Ad-hoc Queries

Click-through rate by keyword

POST /
{
  "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

POST /
{
  "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)

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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_clickorder_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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
// 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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{
  "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

POST /
{ "query": "How many searches did we have last month?" }

Top keywords by revenue

POST /
{ "query": "Top 10 keywords by revenue in June 2026" }

Poor-performing search terms

POST /
{ "query": "Which keywords returned no results last 30 days?" }

Recommendation performance

POST /
{ "query": "Which recommendation widgets drove the most revenue this month?" }

Sales trend

POST /
{ "query": "Show me monthly revenue for 2026" }

Revenue through a specific banner

POST /
{ "query": "What's the revenue for all sales through the Homepage Hero banner?" }

Campaign revenue this month

POST /
{ "query": "Which campaigns drove the most revenue this month?" }

Revenue from autocomplete trending-item clicks

POST /
{ "query": "Revenue from products clicked as autocomplete trending items" }

True zero-result keywords

POST /
{ "query": "Which keywords returned zero results in the last 30 days?" }

Custom sort usage

POST /
{ "query": "Which keywords are searched most with a non-default sort applied?" }

Deep pagination

POST /
{ "query": "Which keywords have visitors paging past page 3 of results?" }

Landing page search volume

POST /
{ "query": "How many searches happened on landing pages last month?" }

Spelling-correction value

POST /
{ "query": "How much revenue came from spelling-corrected searches last month?" }

Search volume by request type

POST /
{ "query": "Break down search volume by request type over the last 30 days" }

Refinement searches

POST /
{ "query": "How many searches used something other than the default search type last 30 days?" }

Revenue by Visitor Target segment

POST /
{ "query": "Show me revenue broken down by visitor segment for the last 30 days" }

Average click position

POST /
{ "query": "What's the average click position by keyword over the last 30 days?" }

Logged-in visitor revenue

POST /
{ "query": "How much revenue came from logged-in visitors last month?" }

Landing page lost opportunity

POST /
{ "query": "Which landing pages had at least 50 searches and an average of 5 or fewer results last 30 days?" }

Keyword lost opportunity

POST /
{ "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

POST /searches
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Unique visitors and conversion, per day

POST /users
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Row-level keyword performance per day

POST /queries
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 500
}

Sitewide click-position distribution

POST /click-positions
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Keywords returning few results

POST /poor-results
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 50
}

Spelling correction activity

POST /spelling
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Revenue by product and keyword

POST /sales
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 100
}

Recommendation widget performance

POST /recommendations
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 50
}

Facet lost opportunity

POST /facets
{
  "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

POST /campaigns
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 50
}

Individual banner performance

POST /banners
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 50
}

Product ratings

POST /product-ratings
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 50
}

Search/conversion by browser and OS

POST /device-browser
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Landing page overview

POST /landing-pages
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "limit": 50
}

Search/conversion by geography

POST /geo-distribution
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Redirect Rule firing counts

POST /redirects
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Per-item search-result impressions / product lost opportunity

POST /product-impressions
{
  "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 of lte 1.
  • Add order_count/revenue to having the same way to find items that are impressed and even clicked, but never actually purchased.
  • Add a filters entry (e.g. { "member": "keyword", "operator": "equals", "values": ["running shoes"] }) to scope the whole report to one keyword instead of the entire catalog.
  • Add "dryRun": true to see the generated SQL without spending an Athena query while you tune thresholds — same convention as the ad-hoc POST / endpoint.
  • Thresholds don't auto-scale with the date rangeimpression_count/click_count are summed across the whole startDateendDate window, 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

POST /search-funnel
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}

Page-load counts by page type and path

POST /page-loads
{
  "startDate": "2026-06-01",
  "endDate": "2026-06-30"
}