SearchIQ API Reference
The SearchIQ API lets you (or your developer) read and manage your store's visual-search data programmatically — check your catalog index, pull search analytics, generate visual recommendations, and read the demand clusters that visual search uncovers. Everything you can do on the dashboard, you can increasingly do by API.
New to APIs? You don't have to use this — the app does everything from its dashboard. The API is for merchants who want to build custom integrations (a BI dashboard, a sourcing pipeline, a headless storefront). If you have a developer, hand them this page; it's all they need.
Shopper privacy comes first. SearchIQ never stores a shopper's search photo, and it never exposes the visual vectors (embeddings) it computes. This API surfaces your merchant data — index status, aggregate analytics, product recommendations, and anonymized demand clusters — and nothing that could identify a shopper or reconstruct a search image. See Privacy on this API before you build.
Base URL
https://searchiq.kernvellabs.com/api/public/v1
All endpoints are under /api/public/v1. All traffic is HTTPS only.
Getting an API key
- In your Shopify admin, open SearchIQ → Settings → API Keys.
- Click Create key, name it (e.g. "Sourcing pipeline"), pick the scopes (permissions) it needs, and optionally an expiry.
- Copy the key immediately — it's shown only once. Store it somewhere safe (a password manager or your server's secrets). If you lose it, revoke it and make a new one.
Keys look like kv_srch_live_xxxxxxxx (live) or kv_srch_test_xxxxxxxx (test mode).
Authentication
Send your key as a Bearer token on every request:
curl https://searchiq.kernvellabs.com/api/public/v1/catalog \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
(X-API-Key: kv_srch_live_xxxxxxxx works too.) Your key is tied to your store — you never pass a shop name. Unauthenticated requests get 401.
Test mode
Use a *_test_* key to build and experiment safely: test mode never sends real emails, never charges, and never mutates real catalog state (a reindex trigger returns its would-be result without spending embedding cost). Test keys are available on every plan, so you can build before you upgrade. Test responses include the header X-Kernvel-Test-Mode: true.
Permissions (scopes)
Each key only does what you allow. Scopes are resource:action, e.g. catalog:read, catalog:write. A request missing the needed scope returns 403 insufficient_scope telling you which scope to add. Grant the least your integration needs.
SearchIQ scopes:
| Scope | Grants |
|---|---|
catalog:read |
Read your catalog index status and recent index jobs. |
catalog:write |
Trigger a catalog re-index. |
search_analytics:read |
Read aggregate visual-search analytics (no shopper data). |
recommendations:read |
Read visual product recommendations ("more like this" / "complete the look"). |
demand_clusters:read |
Read anonymized unmet-demand clusters. |
account:read |
Read your plan, usage, and API limits. |
webhooks:manage |
Create, list, and delete webhook subscriptions. |
There is no customer-PII scope on SearchIQ — the app holds no shopper identities to expose (see Privacy on this API).
Plans
API access is part of your SearchIQ plan. In general: Free = test keys only; Starter = live read access; Growth and above = full read/write (catalog reindex) + webhooks. If an endpoint isn't in your plan you'll get 403 feature_not_in_plan with an upgrade link. Demand clusters are a Scale-and-above capability — reading them on a lower plan returns 403 feature_not_in_plan.
Requests
- Send JSON bodies with
Content-Type: application/json. - Pagination: list endpoints return up to 25 items by default (max 100). Use
?limit=and thenext_cursorfrom the response to page:curl "https://searchiq.kernvellabs.com/api/public/v1/demand_clusters?limit=50&cursor=c2Vfb..." \ -H "Authorization: Bearer kv_srch_live_xxxxxxxx" - Filtering/sorting: each list endpoint documents its filters (e.g.
?status=active). - Idempotency (important for writes): send an
Idempotency-Key: <unique-id>header on anyPOST/PATCH/DELETE. If a request is retried (network hiccup), we return the original result instead of doing it twice — so a retried reindex never kicks off two builds. Required on state-moving actions like a reindex trigger.
Responses
- A single item is returned directly; lists are wrapped:
{ "object": "list", "data": [ { "id": "dmc_8fk3", "object": "demand_cluster", "...": "..." } ], "has_more": true, "next_cursor": "c2Vfb..." } - Money is a string (
"price": "129.00"). Times are UTC ISO-8601 (2026-06-24T10:00:00Z). IDs are stable strings with a type prefix (idx_,idxjob_,dmc_,sp_,shop_,whk_). - Every response includes
X-Request-Id(quote it in support tickets) and rate-limit headers (below).
Rate limits
Each key has a per-minute limit that scales with your plan. Every response includes:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1750671600
If you exceed it you get 429 with a Retry-After header (seconds to wait). Heavier endpoints — recommendations (which runs a vector search) and reindex triggers — carry their own lower sub-limits, noted on each.
Errors
Errors use a standard machine-readable format (application/problem+json):
{ "type": "https://docs.kernvellabs.com/api/errors/insufficient_scope",
"title": "Insufficient scope", "status": 403, "code": "insufficient_scope",
"detail": "This key is missing the required scope 'catalog:write'.",
"errors": [ { "field": "scope", "message": "Required: catalog:write" } ],
"request_id": "req_8fk3a1" }
Common codes: invalid_request, validation_failed, invalid_api_key, expired_api_key, insufficient_scope, feature_not_in_plan, not_found, idempotency_conflict, conflict, rate_limited, service_unavailable, internal_error. The detail, errors, and request_id fields tell you what to fix and what to quote if you need support.
Privacy on this API
This is the rule the whole app is built on, and it shapes what this API will and won't return.
- Shopper search images are never stored. When a shopper searches by photo, the image is turned into a numeric embedding (a "fingerprint"), used to find matches, and then immediately discarded — never written to disk or object storage. There is no endpoint that returns a shopper's search image, because none exists to return.
- Vectors are never exposed. The visual embeddings behind search, recommendations, and demand clusters are an internal implementation detail. No public endpoint ever returns a raw embedding or query vector — not for products, not for searches, not for clusters. You get human-meaningful fields (titles, labels, attributes, counts, similarity scores), never the numeric vectors themselves.
- Search analytics is aggregate-only. The analytics endpoints return counts, rates, latencies, and tallies computed across many searches. They do not return per-search rows, shopper identities, or anything that could single out one shopper. The underlying search records carry no shopper PII — and even that is not exposed on this API.
- Demand clusters are anonymized. A demand cluster is a group of look-alike unmet searches, labelled in plain language (e.g. "olive utility jackets") with a sample count and example attributes. The cluster centroid vector is never returned, and there is no shopper identifier of any kind behind a cluster.
If your integration needs raw event-level data, that is intentionally not available — by design, it does not exist in a shopper-identifiable form.
Webhooks (get notified instead of polling)
Rather than polling, subscribe to events and we'll POST them to your URL the moment they happen.
- Create a subscription (scope
webhooks:manage):
The response includes a signing secret (shown once).curl https://searchiq.kernvellabs.com/api/public/v1/webhooks -X POST \ -H "Authorization: Bearer kv_srch_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/hooks/searchiq", "events": ["index.completed"] }' - We POST an event envelope:
{ "id": "evt_8fk3", "type": "index.completed", "api_version": "2026-06-24", "created_at": "2026-06-24T10:00:00Z", "data": { "...": "the resource" } } - Verify every webhook using the
X-Kernvel-Signature: t=<unix>,v1=<hmac>header — computeHMAC-SHA256(secret, "<t>.<raw-body>")and compare; reject if the timestamp is older than 5 minutes.
We retry failed deliveries with backoff and show a delivery log; persistent failures auto-disable the endpoint and email you. See Webhook events for the full catalog.
Versioning
The API is versioned in the URL (/api/public/v1). We won't make breaking changes to v1 — new fields may be added, but nothing is removed without a new version and 90+ days notice. The X-Kernvel-Api-Version response header shows the dated version applied.
Tools
- OpenAPI spec:
https://searchiq.kernvellabs.com/api/public/v1/openapi.json(import into your tooling). - Postman collection: download the SearchIQ collection, or export the latest one from the API Keys page.
- Quickstart, errors, and webhooks: this reference includes key setup, request format, error shape, signature verification, and event catalog in one page.
Need help?
Email support@kernvellabs.com with the request_id from the response — that lets us find your exact call instantly.
Resources
Catalog Index
Read how far along your visual index is, browse the jobs that built it, and trigger a rebuild. The index is the private, store-scoped collection of "fingerprints" of your product images that visual search compares shopper photos against. Visual search reveals itself to shoppers automatically once your catalog is 80% indexed.
GET /catalog
Get your store's live indexing status — the same data behind the admin Catalog index screen.
- Scope:
catalog:read - Rate limit: standard read limit for your plan.
This endpoint takes no parameters; it returns the single index-status object for your store. (A brand-new install with no index yet returns a synthetic idle status rather than a 404, so you can poll from the moment you install.)
curl https://searchiq.kernvellabs.com/api/public/v1/catalog \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "catalog_index",
"id": "idx_42",
"state": "incremental",
"progress_pct": 100.0,
"search_enabled": true,
"total_products": 8421,
"total_images": 19034,
"embedded_count": 19034,
"failed_count": 0,
"pending_count": 0,
"last_full_index_at": "2026-06-20T04:11:08Z",
"last_incremental_at": "2026-06-24T09:52:31Z",
"est_completion_at": null,
"updated_at": "2026-06-24T09:52:31Z"
}
Field notes:
| Field | Meaning |
|---|---|
state |
Index lifecycle: idle (no index yet), building (full build in progress), incremental (steady state, processing catalog edits), cost_capped (paused at your embedding budget), error (a build failed, retry pending). |
progress_pct |
0–100. Visual search goes live for shoppers at >= 80. |
search_enabled |
true once progress_pct >= 80 — i.e. shoppers can see the widget. |
total_products / total_images |
Catalog size being indexed (a product can have many images). |
embedded_count / pending_count / failed_count |
Images embedded, waiting, and failed. |
last_full_index_at / last_incremental_at / est_completion_at / updated_at |
Lifecycle timestamps (nullable). |
Notable errors: 403 insufficient_scope (needs catalog:read).
GET /catalog/jobs
List recent index jobs — every full rebuild, incremental update, provider-change reindex, and backfill, with its outcome. Mirrors the recent jobs log in the admin.
- Scope:
catalog:read - Cursor-paginated (newest first).
| Query param | Type | Description |
|---|---|---|
limit |
integer | 1–100, default 25. |
cursor |
string | next_cursor from a previous page. |
curl "https://searchiq.kernvellabs.com/api/public/v1/catalog/jobs?limit=2" \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "list",
"data": [
{
"object": "index_job",
"id": "idxjob_1187",
"kind": "incremental",
"status": "completed",
"total_images": 42,
"processed_count": 42,
"embedded_count": 40,
"failed_count": 0,
"skipped_count": 2,
"started_at": "2026-06-24T09:51:55Z",
"completed_at": "2026-06-24T09:52:31Z",
"created_at": "2026-06-24T09:51:54Z"
},
{
"object": "index_job",
"id": "idxjob_1042",
"kind": "bulk_install",
"status": "completed",
"total_images": 19034,
"processed_count": 19034,
"embedded_count": 19034,
"failed_count": 0,
"skipped_count": 0,
"started_at": "2026-06-20T03:40:02Z",
"completed_at": "2026-06-20T04:11:08Z",
"created_at": "2026-06-20T03:39:58Z"
}
],
"has_more": true,
"next_cursor": "c2Vfam9iXzEwNDI"
}
kind is one of bulk_install, incremental, reindex_provider_change, backfill; status is one of queued, running, completed, failed, bulk_failed, cost_capped. Notable errors: 400 invalid_request (bad cursor), 403 insufficient_scope.
GET /catalog/jobs/{job_id}
Fetch a single index job by its id (e.g. idxjob_1187).
- Scope:
catalog:read
curl https://searchiq.kernvellabs.com/api/public/v1/catalog/jobs/idxjob_1187 \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Returns the same index_job object shown above. Notable errors: 404 not_found (no such job on your store), 403 insufficient_scope.
POST /catalog/reindex
Trigger a full catalog re-index — handy after a big catalog change. It's safe to call even if a build is already running: an in-flight build is reported back rather than duplicated, and the idempotency key prevents an accidental double-trigger. The heavy work is queued to a background worker; the index.completed webhook fires from the worker when the build finishes.
- Scope:
catalog:write - Idempotency: send
Idempotency-Key. Required. - Rate limit: low sub-limit (a reindex is a heavy operation).
- Body: none. (Send an empty body, or omit it entirely.)
curl https://searchiq.kernvellabs.com/api/public/v1/catalog/reindex -X POST \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx" \
-H "Idempotency-Key: 3f1c9a2e-7b4d-4c1a-9e2f-1a2b3c4d5e6f"
Example response (202 Accepted — the build runs in the background):
{
"object": "reindex",
"queued": true,
"already_running": false,
"job_id": null,
"state": "building"
}
If a build is already running, you instead get queued: false, already_running: true, and the in-flight job_id:
{
"object": "reindex",
"queued": false,
"already_running": true,
"job_id": "idxjob_1190",
"state": "building"
}
Poll GET /catalog to watch progress_pct climb, or subscribe to the index.completed webhook to be told the moment it finishes. Notable errors: 403 insufficient_scope (needs catalog:write), 403 feature_not_in_plan (live writes require a paid plan), 409 idempotency_conflict (same key, different body), 429 rate_limited, 503 service_unavailable (the indexing queue is temporarily unreachable — retry shortly).
JavaScript (fetch):
const BASE = "https://searchiq.kernvellabs.com/api/public/v1";
const KEY = process.env.SEARCHIQ_API_KEY;
// Check the index, and kick a rebuild only if it isn't already building.
const status = await fetch(`${BASE}/catalog`, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((r) => r.json());
if (status.state !== "building") {
const job = await fetch(`${BASE}/catalog/reindex`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Idempotency-Key": crypto.randomUUID(),
},
}).then((r) => r.json());
console.log("reindex queued:", job.queued, "already running:", job.already_running);
}
Search Analytics
Read aggregate visual-search performance — volume, catalog gaps (no-result rate), click-through, latency, and the styles shoppers convert on. This is the API behind the admin Analytics page.
Aggregate only — by design. Every endpoint here returns numbers computed across many searches. None returns a per-search row, a shopper identity, or a search image/vector. See Privacy on this API. The lookback window is capped by your plan (30 days Starter, 90 Growth, full history Scale and above); request a longer window and it's clamped to your plan's max, and the response tells you the cap in max_days.
GET /search_analytics
The KPI panel: search volume, no-result rate, click-through, latency percentiles, and a daily trend line.
- Scope:
search_analytics:read
| Query param | Type | Description |
|---|---|---|
days |
integer | Lookback window in days (1–365). Clamped to your plan's max_days. Defaults to the plan max. |
curl "https://searchiq.kernvellabs.com/api/public/v1/search_analytics?days=30" \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "search_analytics",
"window_days": 30,
"max_days": 90,
"total_searches": 4820,
"no_result_count": 611,
"no_result_rate": 0.1268,
"click_through_rate": 0.5417,
"avg_latency_ms": 740.2,
"p50_latency_ms": 690.0,
"p95_latency_ms": 1480.0,
"daily": [
{ "date": "2026-06-23", "searches": 168 },
{ "date": "2026-06-24", "searches": 142 }
]
}
Field notes:
| Field | Meaning |
|---|---|
window_days |
The window actually applied (your days request, clamped to max_days). |
max_days |
Your plan's maximum lookback. |
no_result_rate / click_through_rate |
Fractions (0–1). A "no-result" is a search whose best match fell below your store's minimum match strictness — a signal you may be missing what the shopper wanted. |
avg_latency_ms / p50_latency_ms / p95_latency_ms |
Search latency in milliseconds (percentiles nullable). |
daily |
Aggregate per-day series ({date, searches, ...}) — no shopper data. |
Notable errors: 403 insufficient_scope (needs search_analytics:read).
GET /search_analytics/top_styles
The visual attributes of the products shoppers actually click after searching — what's converting in your catalog. Tallied from the auto-discovered tags (category, colour, material, style…) of clicked products.
- Scope:
search_analytics:read
| Query param | Type | Description |
|---|---|---|
days |
integer | Lookback window (1–365), clamped to your plan's max_days. |
limit |
integer | Max values per attribute key (1–50, default 12). |
curl "https://searchiq.kernvellabs.com/api/public/v1/search_analytics/top_styles?days=30&limit=5" \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "top_styles",
"window_days": 30,
"styles": {
"category": [
{ "value": "dress", "count": 318 },
{ "value": "jacket", "count": 204 }
],
"color": [
{ "value": "green", "count": 271 },
{ "value": "black", "count": 188 }
]
}
}
styles groups counts by attribute key (category, color, material, style, …) — counts only, no shopper data. Notable errors: 403 insufficient_scope (needs search_analytics:read).
JavaScript (fetch):
const BASE = "https://searchiq.kernvellabs.com/api/public/v1";
const KEY = process.env.SEARCHIQ_API_KEY;
const overview = await fetch(`${BASE}/search_analytics?days=30`, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((r) => r.json());
console.log(
`${overview.total_searches} searches, ` +
`${(overview.no_result_rate * 100).toFixed(1)}% found nothing (catalog gaps), ` +
`p95 ${overview.p95_latency_ms}ms`
);
Recommendations
Generate visual product recommendations for any product — the same "more like this" and "complete the look" picks shoppers see on your product pages, driven by how products actually look (pattern, colour, shape), not shared tags or co-purchase history. Use it to preview picks, power a headless storefront, or sync recommendations into another channel.
Recommendations are a Growth-and-above capability — calling this on a lower plan returns 403 feature_not_in_plan.
GET /recommendations
Get visual recommendations for one of your products.
- Scope:
recommendations:read - Rate limit: lower sub-limit (each call runs a vector similarity search).
| Query param | Type | Description |
|---|---|---|
product_id |
string | Required. The seed Shopify product id, as sp_<id> or a bare numeric <id>. |
kind |
string | similar (default) for visually similar products, complete_the_look for complementary picks from other categories, or related_on_cart for cart-context picks. |
limit |
integer | How many picks to return (1–50, default 12). |
curl "https://searchiq.kernvellabs.com/api/public/v1/recommendations?product_id=sp_7733120012&kind=similar&limit=3" \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "list",
"data": [
{
"object": "recommendation",
"product_id": "sp_7733120441",
"title": "Olive Utility Field Jacket",
"handle": "olive-utility-field-jacket",
"image_url": "https://cdn.shopify.com/s/files/.../olive-jacket.jpg",
"price": "129.00",
"available_for_sale": true,
"score": 0.91
},
{
"object": "recommendation",
"product_id": "sp_7733120555",
"title": "Sage Canvas Overshirt",
"handle": "sage-canvas-overshirt",
"image_url": "https://cdn.shopify.com/s/files/.../sage-overshirt.jpg",
"price": "98.00",
"available_for_sale": true,
"score": 0.87
}
],
"has_more": false,
"next_cursor": null
}
Field notes:
| Field | Meaning |
|---|---|
score |
Visual closeness, 0–1 (higher = more alike). This is a derived score — never a raw vector. |
available_for_sale |
Whether the pick is currently purchasable; your out-of-stock handling setting still applies on the storefront. |
The picks respect your merchandising (pinned products surface first; buried products are excluded). Notable errors: 400 invalid_request (missing/invalid product_id, or an unknown kind), 403 insufficient_scope (needs recommendations:read), 403 feature_not_in_plan (Growth and above).
GET /recommendations/products
List a sample of your indexed products — the "seed picker" you can choose a product_id from to request recommendations against.
- Scope:
recommendations:read
| Query param | Type | Description |
|---|---|---|
limit |
integer | How many products to sample (1–200, default 60). |
curl "https://searchiq.kernvellabs.com/api/public/v1/recommendations/products?limit=2" \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "list",
"data": [
{
"object": "catalog_product",
"product_id": "sp_7733120012",
"title": "Forest Green Anorak",
"handle": "forest-green-anorak",
"image_url": "https://cdn.shopify.com/s/files/.../anorak.jpg"
},
{
"object": "catalog_product",
"product_id": "sp_7733120441",
"title": "Olive Utility Field Jacket",
"handle": "olive-utility-field-jacket",
"image_url": "https://cdn.shopify.com/s/files/.../olive-jacket.jpg"
}
],
"has_more": false,
"next_cursor": null
}
Notable errors: 403 insufficient_scope (needs recommendations:read), 403 feature_not_in_plan (Growth and above).
JavaScript (fetch):
const BASE = "https://searchiq.kernvellabs.com/api/public/v1";
const KEY = process.env.SEARCHIQ_API_KEY;
const recs = await fetch(
`${BASE}/recommendations?product_id=sp_7733120012&kind=complete_the_look&limit=4`,
{ headers: { Authorization: `Bearer ${KEY}` } }
).then((r) => r.json());
for (const pick of recs.data) {
console.log(`${pick.title} — ${pick.price} (${(pick.score * 100).toFixed(0)}% match)`);
}
Demand Clusters
Read the looks shoppers searched for and couldn't find, grouped into named demand clusters you can stock toward. Every time a shopper snaps a photo and your catalog has no good match, that's a buying signal — SearchIQ clusters those unmet searches into plain-language patterns ("velvet co-ord set," "chunky gold hoops"), ranked by how often shoppers wanted them. This is the API behind the admin Demand intelligence page.
Demand clusters are a Scale-and-above capability — reading them on a lower plan returns 403 feature_not_in_plan.
Anonymized by design. A cluster carries a label, a sample count, and example attributes — never a shopper identity and never the cluster's centroid vector. See Privacy on this API.
GET /demand_clusters
List your store's demand clusters, ranked by demand strength (newest/strongest first).
- Scope:
demand_clusters:read - Cursor-paginated.
| Query param | Type | Description |
|---|---|---|
limit |
integer | 1–100, default 25. |
cursor |
string | next_cursor from a previous page. |
status |
string | Filter by active, dismissed, or resolved. |
curl "https://searchiq.kernvellabs.com/api/public/v1/demand_clusters?status=active&limit=2" \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "list",
"data": [
{
"object": "demand_cluster",
"id": "dmc_318",
"label": "olive utility jackets",
"sample_count": 47,
"status": "active",
"example_attributes": {
"category": "jacket",
"color": "green",
"material": "cotton"
},
"created_at": "2026-06-20T02:15:00Z",
"updated_at": "2026-06-24T02:15:00Z"
},
{
"object": "demand_cluster",
"id": "dmc_402",
"label": "chunky gold hoop earrings",
"sample_count": 12,
"status": "active",
"example_attributes": {
"category": "earrings",
"color": "gold",
"style": "chunky"
},
"created_at": "2026-06-22T02:15:00Z",
"updated_at": "2026-06-24T02:15:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Field notes:
| Field | Meaning |
|---|---|
label |
Plain-language name of the look shoppers wanted, generated from the cluster's example images. |
sample_count |
How many unmet searches fell into this cluster — the demand strength. |
example_attributes |
Aggregated example tags describing the look (category/colour/material…). |
status |
active, dismissed (you hid it), or resolved (you stocked it). |
created_at / updated_at |
When the cluster was first surfaced / last recomputed. |
There is no centroid vector, no per-search detail, and no shopper field — that data is never exposed. Notable errors: 403 insufficient_scope (needs demand_clusters:read), 403 feature_not_in_plan (Scale and above), 400 invalid_request (unknown status filter).
GET /demand_clusters/{cluster_id}
Fetch a single demand cluster by its id (e.g. dmc_318).
- Scope:
demand_clusters:read
curl https://searchiq.kernvellabs.com/api/public/v1/demand_clusters/dmc_318 \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Returns the same demand_cluster object shown above. Notable errors: 404 not_found (no such cluster on your store), 403 insufficient_scope, 403 feature_not_in_plan (Scale and above).
JavaScript (fetch):
const BASE = "https://searchiq.kernvellabs.com/api/public/v1";
const KEY = process.env.SEARCHIQ_API_KEY;
// Pull the strongest unmet-demand clusters to feed a sourcing review.
const clusters = await fetch(`${BASE}/demand_clusters?status=active&limit=10`, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((r) => r.json());
for (const c of clusters.data) {
console.log(`${c.label}: ${c.sample_count} unmet searches`);
}
Tip: Pair this with the StockIQ connection to push these gaps straight into your sourcing workflow — see the Integrations guide for "Assortment gaps from unmet search."
Account
Read your plan, usage, and API limits — useful for showing your own usage UI or guarding against your monthly search/index caps.
GET /account
Your store's plan, status, and API capabilities.
- Scope:
account:read
curl https://searchiq.kernvellabs.com/api/public/v1/account \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "account",
"id": "shop_42",
"shop_domain": "your-store.myshopify.com",
"plan": "scale",
"subscription_status": "active",
"currency": "USD",
"test_mode": false,
"api_access": {
"live_keys": true,
"max_keys": 50,
"scopes": ["account:read", "webhooks:manage", "catalog:read", "catalog:write", "search_analytics:read", "recommendations:read", "demand_clusters:read"],
"rate_per_min": 600,
"webhook_endpoints": 10
}
}
api_access.scopes is the set of scopes your plan permits a key to hold. Notable errors: 403 insufficient_scope (needs account:read).
GET /account/usage
Your current-period usage and rate-limit snapshot — search and index volume against your plan's caps.
- Scope:
account:read
curl https://searchiq.kernvellabs.com/api/public/v1/account/usage \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "usage",
"plan": "scale",
"indexed_products": 8421,
"max_indexed_products": 50000,
"searches_this_month": 38211,
"max_searches_per_month": 100000,
"rate_limit": 600,
"rate_limit_remaining": 598
}
Field notes: searches_this_month counts every visual search this calendar month (storefront and in-admin playground both count) against max_searches_per_month. indexed_products is your current indexed-product count against max_indexed_products. Notable errors: 403 insufficient_scope.
JavaScript (fetch):
const BASE = "https://searchiq.kernvellabs.com/api/public/v1";
const KEY = process.env.SEARCHIQ_API_KEY;
const usage = await fetch(`${BASE}/account/usage`, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((r) => r.json());
const pct = ((usage.searches_this_month / usage.max_searches_per_month) * 100).toFixed(1);
console.log(`Searches this month: ${usage.searches_this_month}/${usage.max_searches_per_month} (${pct}%)`);
Webhooks
Manage the endpoints we POST events to. See Webhook events for the event catalog and Webhooks (get notified instead of polling) for signature verification.
POST /webhooks
Create a subscription. The response includes the signing secret exactly once — store it; you can't retrieve it again.
- Scope:
webhooks:manage - The destination URL is validated at create time: it must be
httpsand resolve to a public IP (loopback/private/metadata addresses are rejected). The number of endpoints is capped by your plan'swebhook_endpoints. - New subscriptions return
201. Exact retries with the same URL, live/test environment, and event set return200with the existing subscription and an Idempotent-Replay: true response header. The signing secret is NOT re-returned on a replay (it is shown only once, on the original 201 create) — reuse the secret you stored then.
| Body field | Type | Description |
|---|---|---|
url |
string | Required. Your HTTPS endpoint. |
events |
string[] | Required, ≥1. Event types to subscribe to (see Webhook events). |
description |
string | Optional label (≤255 chars). |
curl https://searchiq.kernvellabs.com/api/public/v1/webhooks -X POST \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/hooks/searchiq",
"events": ["index.completed", "demand.cluster_ready"],
"description": "Ops pipeline" }'
Example response (201 Created):
{
"object": "webhook_subscription",
"id": "whk_17",
"url": "https://yourapp.com/hooks/searchiq",
"events": ["index.completed", "demand.cluster_ready"],
"description": "Ops pipeline",
"env": "live",
"is_active": true,
"created_at": "2026-06-24T10:00:00Z",
"signing_secret": "whsec_3f1c9a2e7b4d4c1a9e2f1a2b3c4d5e6f"
}
Notable errors: 400 invalid_request (bad URL), 422 invalid_request (unknown event type in events), 403 insufficient_scope (needs webhooks:manage), 403 feature_not_in_plan (webhooks aren't on your plan, or you've hit your endpoint cap).
GET /webhooks
List your subscriptions (the signing secret is never returned again).
- Scope:
webhooks:manage
curl https://searchiq.kernvellabs.com/api/public/v1/webhooks \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{
"object": "list",
"data": [
{
"object": "webhook_subscription",
"id": "whk_17",
"url": "https://yourapp.com/hooks/searchiq",
"events": ["index.completed", "demand.cluster_ready"],
"description": "Ops pipeline",
"env": "live",
"is_active": true,
"created_at": "2026-06-24T10:00:00Z"
}
],
"has_more": false,
"next_cursor": null
}
GET /webhooks/{webhook_id}
Fetch a single subscription by its id (the signing secret is never returned again).
- Scope:
webhooks:manage
curl https://searchiq.kernvellabs.com/api/public/v1/webhooks/whk_17 \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Returns the same webhook_subscription object shown above (without signing_secret). Notable errors: 404 not_found (no such subscription on your store), 403 insufficient_scope.
DELETE /webhooks/{webhook_id}
Delete a subscription. We stop delivering immediately.
- Scope:
webhooks:manage
curl https://searchiq.kernvellabs.com/api/public/v1/webhooks/whk_17 -X DELETE \
-H "Authorization: Bearer kv_srch_live_xxxxxxxx"
Example response:
{ "object": "webhook_subscription", "id": "whk_17", "deleted": true }
Notable errors: 404 not_found (no such subscription on your store), 403 insufficient_scope.
JavaScript (fetch):
const BASE = "https://searchiq.kernvellabs.com/api/public/v1";
const KEY = process.env.SEARCHIQ_API_KEY;
const sub = await fetch(`${BASE}/webhooks`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://yourapp.com/hooks/searchiq",
events: ["index.completed", "demand.cluster_ready"],
}),
}).then((r) => r.json());
// Persist sub.signing_secret now — it is never shown again.
console.log("subscription:", sub.id);
Webhook events
Subscribe to these so you never have to poll. Every delivery is signed (X-Kernvel-Signature) and arrives as the standard envelope: { id, type, api_version, created_at, data }. Test-mode subscriptions receive only test-mode events.
| Event | Fires when |
|---|---|
index.completed |
A full or incremental index run finishes. data is the catalog-index object, including the final counts. |
demand.cluster_ready |
The demand-intel sweep surfaces a new (or newly strengthened) demand cluster. Scale and above. data is the demand-cluster object. |
Sample index.completed payload
{
"id": "evt_9a2f1c",
"type": "index.completed",
"api_version": "2026-06-24",
"created_at": "2026-06-24T04:11:09Z",
"data": {
"object": "catalog_index",
"id": "idx_42",
"state": "incremental",
"progress_pct": 100.0,
"search_enabled": true,
"total_products": 8421,
"total_images": 19034,
"embedded_count": 19034,
"failed_count": 0,
"pending_count": 0,
"last_full_index_at": "2026-06-24T04:11:08Z",
"last_incremental_at": "2026-06-24T04:11:08Z"
}
}
Sample demand.cluster_ready payload
{
"id": "evt_4b7d2e",
"type": "demand.cluster_ready",
"api_version": "2026-06-24",
"created_at": "2026-06-24T02:15:01Z",
"data": {
"object": "demand_cluster",
"id": "dmc_318",
"label": "olive utility jackets",
"sample_count": 47,
"status": "active",
"example_attributes": {
"category": "jacket",
"color": "green",
"material": "cotton"
},
"created_at": "2026-06-20T02:15:00Z",
"updated_at": "2026-06-24T02:15:00Z"
}
}
Note what is not in either payload: no shopper image, no embedding/vector, no shopper identifier. That holds for every SearchIQ webhook — see Privacy on this API.