Analytics Events
Overview
Seekora Analytics is a high-throughput ingest endpoint for storefront and search-attribution events. There are two supported paths to submit events, and both terminate at the same ingest endpoint and emit the same canonical event names:
| Path | When to use | Package |
|---|---|---|
| Seekora SDK | You're calling Seekora search from JS/TS — @seekora-ai/search-sdk gives you one-line trackXxx helpers, and @seekora-ai/ui-sdk-react fires the search-lifecycle events automatically when you mount <SearchProvider>. | @seekora-ai/search-sdk, @seekora-ai/ui-sdk-react |
| Direct REST | Non-JS storefronts, server-side workers, mobile native shells, Liquid themes — anywhere you want to BYO transport and POST directly to the ingest endpoint. | None — plain HTTPS |
Endpoint
Seekora exposes exactly one public ingest path. Browser CORS is enabled on /v1/b only.
| Environment | URL |
|---|---|
| Stage | https://analytics-ingest.seekora.ai/v1/b |
| Production | https://analytics-ingest.seekora.ai/v1/b |
Successful response
{ "ok": true, "okEvents": 5, "receivedEvents": 5 }
HTTP 200. okEvents equals the count of events in the batch that passed envelope-level validation at ingest; receivedEvents mirrors the size of the batch you sent. A 200 here means the batch was accepted — downstream processing runs asynchronously.
Error responses
{ "error": "error parsing message: ..." }
Returned with HTTP 400 when the body is malformed JSON or fails envelope validation.
Authentication
The ingest endpoint authenticates by a per-tenant writeKey carried at the top level of the request body. There is no Authorization header and no Bearer token on this surface — the writeKey alone identifies your tenant and store.
| Where to put the writeKey | Top-level writeKey field on the request body |
| Where to find your writeKey | Admin UI → Stores → Welcome → Events Write Key row |
| Programmatic retrieval | GET /api/v3/account/analytics/writekey |
See the Configuration guide for the full retrieval flow.
Request envelope
The top-level body shape is:
{
"writeKey": "<your-events-write-key>",
"batch": [ /* one or more events */ ],
"sentAt": "2026-05-31T14:50:00.500Z"
}
| Field | Type | Required | Notes |
|---|---|---|---|
writeKey | string (UUID) | yes | Per-tenant ingest credential. Identifies the tenant and store. |
batch | array of event objects | yes | One or more events — see per-event shape below. |
sentAt | string (ISO-8601) | yes | Wall-clock time when the client flushed the batch. Set this close to send time so ingest can correct for client clock drift. |
Event fields
Each entry in batch is an event object with these fields:
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | yes | Use "track" for the canonical events listed below. |
event | string | yes | One of the canonical event names — see the table below. |
anonymousId | string (UUID) | yes | Client-generated, stable per browser/session. |
messageId | string (UUID) | yes | Unique per event — used for downstream dedup. |
timestamp | string (ISO-8601) | yes | When the event occurred. |
properties | object | yes | Per-event payload — search_id, query, object_id, etc. |
writeKey | string (UUID) | yes | The same writeKey as the envelope; restated here for per-event routing. |
userId | string | no | Set when the visitor is logged in. Used for identity stitching. |
Canonical event names
The 18-event catalog below is the public event surface Seekora recognises. Sending an event name outside this list is silently dropped after ingest. Keep this set exact.
| Event | Fired when |
|---|---|
Search Submitted | The visitor submits a search query. |
Search Refined | The visitor changes or re-runs the active query (e.g. typing more, hitting search again). |
Search Empty | The query returns zero results. |
Search Impression | A search results surface is rendered to the visitor. |
Filter Viewed | A filter / refinement UI is presented to the visitor. |
Filter Converted | The visitor commits a filter change that re-runs the search. |
Facet Applied | A facet value is toggled (subset of Filter Converted that names the facet). |
Object Viewed | A search result card / object enters the visitor's viewport. |
Object Clicked | The visitor clicks a search result. |
Object Clicked Outside Search | The visitor clicks a Seekora-rendered object outside a search results context (e.g. a recommendation strip on a PDP). |
Object Converted | The visitor performs a high-intent action on an object surfaced via search (e.g. add to cart, purchase). |
Object Converted Outside Search | Same as above, but the object originated outside a search context. |
Product Added | The visitor adds a product to cart. |
Product List Viewed | A product list (category page, results page) renders for the visitor. |
Order Completed | The visitor finishes checkout. |
Suggestion Impression | A query suggestion is rendered to the visitor. |
Suggestion Clicked | The visitor clicks a query suggestion. |
Cart Viewed | The visitor views their cart. |
Every search-attributed event (anything in the Search / Filter / Object / Suggestion families) should carry a stable search_id in properties so the downstream funnel can stitch the journey. See "Best practices" below.
Direct REST examples
The following single-event payload is the minimal verified shape that the ingest accepts. Replace <your-events-write-key>, <your-anonymous-id>, and <unique-uuid-per-event> with your values:
curl -X POST 'https://analytics-ingest.seekora.ai/v1/b' \
-H 'Content-Type: application/json' \
--data-raw '{
"writeKey": "<your-events-write-key>",
"batch": [
{
"type": "track",
"event": "Search Submitted",
"anonymousId": "<your-anonymous-id>",
"messageId": "<unique-uuid-per-event>",
"timestamp": "2026-05-31T14:50:00.000Z",
"properties": {
"search_id": "<search-uuid>",
"query": "shoes",
"num_found": 12,
"surface": "results_page"
},
"writeKey": "<your-events-write-key>"
}
],
"sentAt": "2026-05-31T14:50:00.500Z"
}'
A successful response is {"ok":true,"okEvents":1,"receivedEvents":1} with HTTP 200.
The same envelope works for every event in the catalog — only event and properties change. Three more examples below; each is a standalone batch you can POST identically.
Search Impression
{
"writeKey": "<your-events-write-key>",
"batch": [
{
"type": "track",
"event": "Search Impression",
"anonymousId": "<your-anonymous-id>",
"messageId": "<unique-uuid-per-event>",
"timestamp": "2026-05-31T14:50:01.000Z",
"properties": {
"search_id": "<search-uuid>",
"object_ids": ["prod_1001", "prod_1002", "prod_1003"]
},
"writeKey": "<your-events-write-key>"
}
],
"sentAt": "2026-05-31T14:50:01.500Z"
}
Object Clicked
{
"writeKey": "<your-events-write-key>",
"batch": [
{
"type": "track",
"event": "Object Clicked",
"anonymousId": "<your-anonymous-id>",
"messageId": "<unique-uuid-per-event>",
"timestamp": "2026-05-31T14:50:03.000Z",
"properties": {
"search_id": "<search-uuid>",
"object_id": "prod_1001",
"position": 1
},
"writeKey": "<your-events-write-key>"
}
],
"sentAt": "2026-05-31T14:50:03.500Z"
}
Order Completed
{
"writeKey": "<your-events-write-key>",
"batch": [
{
"type": "track",
"event": "Order Completed",
"anonymousId": "<your-anonymous-id>",
"userId": "user_8294",
"messageId": "<unique-uuid-per-event>",
"timestamp": "2026-05-31T14:50:05.000Z",
"properties": {
"search_id": "<search-uuid>",
"order_id": "order_55821",
"revenue": 199.99,
"currency": "USD",
"products": [{ "object_id": "prod_1001", "quantity": 1, "price": 199.99 }]
},
"writeKey": "<your-events-write-key>"
}
],
"sentAt": "2026-05-31T14:50:05.500Z"
}
In production you'll typically batch many events into one request — see "Best practices" below.
SDK examples
@seekora-ai/search-sdk (TypeScript)
The search SDK exports a track* helper per canonical event name. Pass a ctx object with your writeKey, ingestHost, and (optionally) userId; the helpers fill in type, messageId, timestamp, and the canonical event name for you.
import {
trackSearchSubmitted,
trackObjectClicked,
} from '@seekora-ai/search-sdk';
const ctx = {
writeKey: 'YOUR_WRITE_KEY',
ingestHost: 'https://analytics-ingest.seekora.ai',
userId: undefined, // set when the visitor is logged in
};
// Fire when the visitor submits a search:
trackSearchSubmitted(ctx, {
search_id: 'search-abc-001',
query: 'wireless headphones',
num_found: 47,
surface: 'search-page',
});
// Fire on result click:
trackObjectClicked(ctx, {
search_id: 'search-abc-001',
object_id: 'prod_1001',
position: 1,
});
@seekora-ai/ui-sdk-react (TSX)
If you're rendering Seekora's <SearchProvider> / <SearchBox> / <Hits> family, the SDK fires Search Submitted, Search Refined, Search Empty, Search Impression, Object Viewed, Object Clicked, and Suggestion Impression / Clicked automatically. Wire it once at the provider boundary:
import { SearchProvider, SearchBox, Hits } from '@seekora-ai/ui-sdk-react';
export function StorefrontSearch() {
return (
<SearchProvider
trackContext={{
writeKey: 'YOUR_WRITE_KEY',
ingestHost: 'https://analytics-ingest.seekora.ai',
}}
>
<SearchBox />
<Hits />
</SearchProvider>
);
}
Cart / checkout events (Product Added, Cart Viewed, Order Completed) live outside the search surface — fire those yourself from your checkout flow using the search-sdk track* helpers.
Best practices
- Always batch. Buffer events client-side and send via
/v1/b. Browser CORS is only enabled on/v1/b. - Set
sentAtclose to actual send time. Ingest uses the gap betweensentAtand eventtimestampto correct for client clock drift. - Always carry
search_id. Every Search-family, Filter-family, Object-family, and Suggestion-family event should reuse the samesearch_idacross the journey so the downstream funnel stitches correctly. messageIdmust be unique per event. Downstream dedup uses it — reusing one collapses two events into one.- Don't invent event names. Anything outside the canonical catalog above is dropped silently after ingest validation.
- One writeKey per tenant. Do not share writeKeys across orgs.