ListIQ API Reference
The ListIQ API lets you (or your developer) read and manage your store's ListIQ data programmatically — build custom dashboards, sync wishlists into your CRM or warehouse system, trigger campaigns from your own tooling, or pull demand insights into a BI tool. Everything you can do in the app, 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. If you have a developer, hand them this page; it's all they need.
Base URL
https://listiq.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 ListIQ → Settings → API Keys.
- Click Create key, name it (e.g. "Warehouse sync"), 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_lst_live_xxxxxxxx (live) or kv_lst_test_xxxxxxxx (test mode).
Authentication
Send your key as a Bearer token on every request:
curl https://listiq.kernvellabs.com/api/public/v1/wishlists \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
(X-API-Key: kv_lst_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, never queues real customer notifications. 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. wishlists:read, wishlist_items:write. A request missing the needed scope returns 403 insufficient_scope telling you which scope to add. Grant the least your integration needs. Customer personal data sits behind its own scope (customers:read) and is only available on higher plans.
ListIQ scopes:
| Scope | Grants |
|---|---|
account:read |
Read your plan, usage, and limits (every key) |
wishlists:read |
Read wishlists |
wishlist_items:read |
Read the items on a wishlist |
wishlist_items:write |
Add and remove wishlist items |
subscribers:read |
Read back-in-stock subscribers (masked email, no raw PII) |
customers:read |
Read customer records, including contact details (PII — top plans only) |
campaigns:read |
Read email campaigns and their stats |
campaigns:write |
Create and send email campaigns |
demand_insights:read |
Read demand-pulse insight reports |
webhooks:manage |
Create, list, and delete webhook subscriptions |
Plans
API access is part of your ListIQ plan. In general: Free = test keys only (read scopes for sandboxing); Starter = read access on live keys (no writes, webhooks, or PII); Pro / Business = full read/write + webhooks + customer PII. If an endpoint isn't in your plan you'll get 403 feature_not_in_plan with an upgrade link.
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://listiq.kernvellabs.com/api/public/v1/wishlists?limit=50&cursor=c2Vfb..." \ -H "Authorization: Bearer kv_lst_live_xxxxxxxx" - Filtering/sorting: each list endpoint documents its filters (e.g. campaigns accept
?status=completed). - Idempotency (important for writes): send an
Idempotency-Key: <unique-id>header on anyPOST/DELETE. If a request is retried (network hiccup), we return the original result instead of doing it twice — so a retried "add item", "create campaign", or "send campaign" never happens twice. Recommended on every state-moving action.
Responses
- A single item is returned directly; lists are wrapped:
{ "object": "list", "data": [ { "id": "wl_8fk3a1b2", "object": "wishlist", "...": "..." } ], "has_more": true, "next_cursor": "c2Vfb..." } - Money is a string with a currency (
"price_when_added": "120.00", "currency": "USD"). Times are UTC ISO-8601 (2026-06-24T10:00:00Z). IDs are stable strings with a type prefix (wl_,wsi_,sub_,cust_,camp_,di_,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 (60/min Free, 120/min Starter, 300/min Pro, 600/min Business). 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).
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 'wishlist_items:write'.",
"errors": [ { "field": "scope", "message": "Required: wishlist_items: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, unprocessable, rate_limited, internal_error. The detail, errors, and request_id fields tell you what to fix and what to quote if you need support.
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://listiq.kernvellabs.com/api/public/v1/webhooks -X POST \ -H "Authorization: Bearer kv_lst_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/hooks/listiq", "events": ["wishlist.item_added"] }' - We POST an event envelope:
{ "id": "evt_8fk3", "type": "wishlist.item_added", "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 ListIQ 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://listiq.kernvellabs.com/api/public/v1/openapi.json(import into your tooling). - Liveness probe: unauthenticated
GET https://listiq.kernvellabs.com/api/public/v1/ping. - Postman collection: download the ListIQ 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
ListIQ exposes the data that makes wishlists pay back: the wishlists your shoppers build, the individual wishlist items they save, your back-in-stock subscribers, your customers (PII), your email campaigns, and the demand insights ListIQ computes from cross-store wishlist activity. Below: every endpoint, its scope, params, a copy-paste curl, an example response, and the errors worth handling.
Wishlists
A wishlist is a named list of products a customer (or guest) saved on your store. Read them to power custom dashboards, sync saved-product data into your CRM, or build "most-wishlisted" merchandising.
GET /wishlists
List wishlists for your store, newest first. Cursor-paginated.
Scope: wishlists:read
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25. |
cursor |
query | string | next_cursor from a previous page. |
curl "https://listiq.kernvellabs.com/api/public/v1/wishlists?limit=2" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "wishlist",
"id": "wl_8fk3a1b2",
"name": "Holiday gift ideas",
"visibility": "public",
"is_default": false,
"item_count": 7,
"has_customer": true,
"created_at": "2026-06-18T14:03:11Z",
"updated_at": "2026-06-22T09:51:40Z"
},
{
"object": "wishlist",
"id": "wl_3a90fe17",
"name": "My Wishlist",
"visibility": "private",
"is_default": true,
"item_count": 2,
"has_customer": false,
"created_at": "2026-06-17T11:22:05Z",
"updated_at": "2026-06-17T11:40:18Z"
}
],
"has_more": true,
"next_cursor": "c2Vfd2xfM2E5MGZlMTc"
}
The has_customer flag tells you whether the wishlist belongs to a logged-in customer (true) or an anonymous guest session (false) — the customer's identity itself isn't exposed here (read /customers with customers:read for that).
Notable errors: 400 invalid_request (malformed cursor), 403 insufficient_scope (missing wishlists:read).
GET /wishlists/{wishlist_id}
Fetch a single wishlist by id.
Scope: wishlists:read
| Param | In | Type | Description |
|---|---|---|---|
wishlist_id |
path | string | The wishlist id (wl_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/wishlists/wl_8fk3a1b2" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "wishlist",
"id": "wl_8fk3a1b2",
"name": "Holiday gift ideas",
"visibility": "public",
"is_default": false,
"item_count": 7,
"has_customer": true,
"created_at": "2026-06-18T14:03:11Z",
"updated_at": "2026-06-22T09:51:40Z"
}
Notable errors: 404 not_found (no such wishlist, or it belongs to another store — we return 404, never 403, so we never reveal that another store's id exists).
JavaScript (fetch) example — list wishlists:
const res = await fetch(
"https://listiq.kernvellabs.com/api/public/v1/wishlists?limit=50",
{ headers: { Authorization: "Bearer kv_lst_live_xxxxxxxx" } }
);
if (!res.ok) {
const problem = await res.json();
throw new Error(`${problem.code}: ${problem.detail}`);
}
const { data, has_more, next_cursor } = await res.json();
console.log(`Got ${data.length} wishlists, more: ${has_more}`);
Wishlist Items
The individual products saved inside a wishlist — with the price when added, current price, and availability. This is the data behind "most-wishlisted products."
GET /wishlists/{wishlist_id}/items
List the items in a wishlist, newest first.
Scope: wishlist_items:read
| Param | In | Type | Description |
|---|---|---|---|
wishlist_id |
path | string | The wishlist id (wl_...). |
limit |
query | integer | 1–100, default 25. |
cursor |
query | string | next_cursor from a previous page. |
curl "https://listiq.kernvellabs.com/api/public/v1/wishlists/wl_8fk3a1b2/items" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "wishlist_item",
"id": "wsi_71c4d8a0",
"wishlist_id": "wl_8fk3a1b2",
"shopify_product_id": "8042113990741",
"shopify_variant_id": "44910292828245",
"product_title": "Merino Wool Overshirt",
"variant_title": "Charcoal / M",
"product_handle": "merino-wool-overshirt",
"image_url": "https://cdn.shopify.com/s/files/1/0001/overshirt.jpg",
"currency": "USD",
"price_when_added": "129.00",
"current_price": "99.00",
"is_available": true,
"priority": 4,
"created_at": "2026-06-19T08:30:22Z"
}
],
"has_more": false,
"next_cursor": null
}
shopify_product_id and shopify_variant_id are returned as strings (Shopify ids exceed 32-bit range).
Notable errors: 404 not_found (wishlist not found / not yours), 403 insufficient_scope (missing wishlist_items:read).
GET /wishlists/{wishlist_id}/items/{item_id}
Fetch a single item within a wishlist.
Scope: wishlist_items:read
| Param | In | Type | Description |
|---|---|---|---|
wishlist_id |
path | string | The wishlist id (wl_...). |
item_id |
path | string | The wishlist item id (wsi_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/wishlists/wl_8fk3a1b2/items/wsi_71c4d8a0" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "wishlist_item",
"id": "wsi_71c4d8a0",
"wishlist_id": "wl_8fk3a1b2",
"shopify_product_id": "8042113990741",
"shopify_variant_id": "44910292828245",
"product_title": "Merino Wool Overshirt",
"variant_title": "Charcoal / M",
"product_handle": "merino-wool-overshirt",
"image_url": "https://cdn.shopify.com/s/files/1/0001/overshirt.jpg",
"currency": "USD",
"price_when_added": "129.00",
"current_price": "99.00",
"is_available": true,
"priority": 4,
"created_at": "2026-06-19T08:30:22Z"
}
Notable errors: 404 not_found (wishlist or item not found / not yours).
POST /wishlists/{wishlist_id}/items
Add a product to a wishlist. Returns 201. Idempotent — send an Idempotency-Key so a retried add never duplicates.
Scope: wishlist_items:write
| Field | Type | Required | Description |
|---|---|---|---|
shopify_product_id |
integer | yes | The Shopify product id to add. |
shopify_variant_id |
integer | no | A specific variant; omit for the default. |
product_title |
string | no | Product title (≤500 chars). |
variant_title |
string | no | Variant title (≤500 chars). |
product_handle |
string | no | Product handle (≤500 chars). |
product_image_url |
string | no | Product image URL. |
price_when_added |
number | no | Price at the moment of adding (≥0). |
currency_code |
string | no | 3-letter currency code. Defaults to USD. |
notes |
string | no | A short customer note (≤2000 chars). |
priority |
integer | no | 1–5 (5 = highest). Defaults to 3. |
Unknown fields are rejected (422 unprocessable).
curl "https://listiq.kernvellabs.com/api/public/v1/wishlists/wl_8fk3a1b2/items" \
-X POST \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9c1d8e2a-4f7b-4f0a-9d2c-1a2b3c4d5e6f" \
-d '{ "shopify_product_id": 8042113990741, "shopify_variant_id": 44910292828245, "priority": 4 }'
{
"object": "wishlist_item",
"id": "wsi_71c4d8a0",
"wishlist_id": "wl_8fk3a1b2",
"shopify_product_id": "8042113990741",
"shopify_variant_id": "44910292828245",
"product_title": "Merino Wool Overshirt",
"variant_title": "Charcoal / M",
"product_handle": "merino-wool-overshirt",
"image_url": null,
"currency": "USD",
"price_when_added": "99.00",
"current_price": "99.00",
"is_available": true,
"priority": 4,
"created_at": "2026-06-24T10:00:00Z"
}
Adding an item fires the wishlist.item_added webhook.
Notable errors: 404 not_found (wishlist not found / not yours), 409 conflict (that product/variant is already on this wishlist, or you've hit your plan's per-wishlist item cap), 409 idempotency_conflict (same Idempotency-Key reused with a different body), 422 unprocessable (unknown field in the body).
DELETE /wishlists/{wishlist_id}/items/{item_id}
Remove an item from a wishlist. Returns 200. Idempotent.
Scope: wishlist_items:write
| Param | In | Type | Description |
|---|---|---|---|
wishlist_id |
path | string | The wishlist id (wl_...). |
item_id |
path | string | The wishlist item id (wsi_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/wishlists/wl_8fk3a1b2/items/wsi_71c4d8a0" \
-X DELETE \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx" \
-H "Idempotency-Key: 2b7f1c90-77aa-4d3e-9b11-0c9e8f7a6b5c"
{ "object": "wishlist_item", "id": "wsi_71c4d8a0", "deleted": true }
Notable errors: 404 not_found (wishlist or item not found / not yours).
JavaScript (fetch) example — add an item idempotently:
const res = await fetch(
"https://listiq.kernvellabs.com/api/public/v1/wishlists/wl_8fk3a1b2/items",
{
method: "POST",
headers: {
Authorization: "Bearer kv_lst_live_xxxxxxxx",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ shopify_product_id: 8042113990741, priority: 4 }),
}
);
const item = await res.json();
console.log(item.id, item.current_price);
Back-in-Stock Subscribers
A back-in-stock subscriber is a store customer who opted into restock alerts (accepts_back_in_stock = true). Read them to size restock decisions or sync interest into your planning tools.
This resource is deliberately non-PII: it exposes the subscription status, wishlist counts, a masked email (
ab***@example.com), and a stable Shopify customer id — never the raw email or name. Full contact records live behind the PII-gated/customersresource (customers:read).
GET /subscribers
List back-in-stock subscribers across your store, newest first.
Scope: subscribers:read
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25. |
cursor |
query | string | next_cursor from a previous page. |
curl "https://listiq.kernvellabs.com/api/public/v1/subscribers?limit=2" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "subscriber",
"id": "sub_4ab19c70",
"shopify_customer_id": "6203411984533",
"email_masked": "sh***@example.com",
"accepts_back_in_stock": true,
"total_wishlisted": 14,
"last_active_at": "2026-06-22T18:44:10Z",
"created_at": "2026-06-19T08:30:22Z"
}
],
"has_more": false,
"next_cursor": null
}
The email is always masked here regardless of your scopes — to get the raw contact record, read /customers with the customers:read PII scope.
Notable errors: 403 insufficient_scope (missing subscribers:read).
GET /subscribers/{subscriber_id}
Fetch a single back-in-stock subscriber.
Scope: subscribers:read
| Param | In | Type | Description |
|---|---|---|---|
subscriber_id |
path | string | The subscriber id (sub_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/subscribers/sub_4ab19c70" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "subscriber",
"id": "sub_4ab19c70",
"shopify_customer_id": "6203411984533",
"email_masked": "sh***@example.com",
"accepts_back_in_stock": true,
"total_wishlisted": 14,
"last_active_at": "2026-06-22T18:44:10Z",
"created_at": "2026-06-19T08:30:22Z"
}
Notable errors: 404 not_found (no such subscriber / not yours).
JavaScript (fetch) example — count subscribers waiting:
async function subscriberCount() {
let count = 0, cursor = null;
do {
const url = new URL("https://listiq.kernvellabs.com/api/public/v1/subscribers");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: "Bearer kv_lst_live_xxxxxxxx" } });
const page = await res.json();
count += page.data.length;
cursor = page.next_cursor;
} while (cursor);
return count;
}
console.log(await subscriberCount());
Customers
Your shoppers, with the wishlist signals ListIQ tracks: how much they've wishlisted and how much they've bought. Contact details (email, name) are personal data (PII) and require the customers:read scope, available on top plans only.
GET /customers
List customers, newest first.
Scope: customers:read (PII)
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25. |
cursor |
query | string | next_cursor from a previous page. |
curl "https://listiq.kernvellabs.com/api/public/v1/customers?limit=2" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "customer",
"id": "cust_5d21c0a9",
"shopify_customer_id": "6203411984533",
"email": "shopper@example.com",
"first_name": "Priya",
"last_name": "Shah",
"accepts_back_in_stock": true,
"total_wishlisted": 14,
"total_purchased": 3,
"last_active_at": "2026-06-22T18:44:10Z",
"created_at": "2026-03-02T12:10:00Z"
}
],
"has_more": true,
"next_cursor": "c2VfY3VzdF81ZDIxYzBhOQ"
}
Notable errors: 403 insufficient_scope (missing customers:read), 403 feature_not_in_plan (PII access is a top-plan capability).
GET /customers/{customer_id}
Fetch a single customer.
Scope: customers:read (PII)
| Param | In | Type | Description |
|---|---|---|---|
customer_id |
path | string | The customer id (cust_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/customers/cust_5d21c0a9" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "customer",
"id": "cust_5d21c0a9",
"shopify_customer_id": "6203411984533",
"email": "shopper@example.com",
"first_name": "Priya",
"last_name": "Shah",
"accepts_back_in_stock": true,
"total_wishlisted": 14,
"total_purchased": 3,
"last_active_at": "2026-06-22T18:44:10Z",
"created_at": "2026-03-02T12:10:00Z"
}
Notable errors: 404 not_found (no such customer / not yours), 403 insufficient_scope (missing customers:read).
JavaScript (fetch) example — page through customers:
const res = await fetch(
"https://listiq.kernvellabs.com/api/public/v1/customers?limit=100",
{ headers: { Authorization: "Bearer kv_lst_live_xxxxxxxx" } }
);
if (res.status === 403) throw new Error("customers:read (PII) scope required — upgrade plan");
const { data } = await res.json();
console.log(data.map((c) => c.email));
Campaigns
The email campaigns ListIQ sends — price-drop alerts, back-in-stock notices, low-stock nudges, reminders, AI recommendations, and custom blasts — with their send/open/click stats. Read them for reporting, create a campaign as a draft, then send it.
GET /campaigns
List campaigns, newest first.
Scope: campaigns:read
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25. |
cursor |
query | string | next_cursor from a previous page. |
status |
query | string | Filter by draft, scheduled, sending, completed, failed, paused, or cancelled. |
curl "https://listiq.kernvellabs.com/api/public/v1/campaigns?status=completed" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "campaign",
"id": "camp_a17fb920",
"name": "June price-drop blast",
"subject": "The item you saved just dropped in price",
"campaign_type": "price_drop",
"status": "completed",
"total_sent": 1840,
"total_opened": 962,
"total_clicked": 311,
"total_failed": 6,
"ai_generated": true,
"scheduled_at": "2026-06-20T09:00:00Z",
"sent_at": "2026-06-20T09:00:14Z",
"completed_at": "2026-06-20T09:07:51Z",
"created_at": "2026-06-19T17:22:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Notable errors: 400 invalid_request (unknown status filter), 403 insufficient_scope (missing campaigns:read).
GET /campaigns/{campaign_id}
Fetch a single campaign with its full stats.
Scope: campaigns:read
| Param | In | Type | Description |
|---|---|---|---|
campaign_id |
path | string | The campaign id (camp_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/campaigns/camp_a17fb920" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "campaign",
"id": "camp_a17fb920",
"name": "June price-drop blast",
"subject": "The item you saved just dropped in price",
"campaign_type": "price_drop",
"status": "completed",
"total_sent": 1840,
"total_opened": 962,
"total_clicked": 311,
"total_failed": 6,
"ai_generated": true,
"scheduled_at": "2026-06-20T09:00:00Z",
"sent_at": "2026-06-20T09:00:14Z",
"completed_at": "2026-06-20T09:07:51Z",
"created_at": "2026-06-19T17:22:00Z"
}
Notable errors: 404 not_found (no such campaign / not yours).
POST /campaigns
Create a campaign. It is always created as a draft; send it afterwards with POST /campaigns/{campaign_id}/send. Returns 201. Idempotent — send an Idempotency-Key so a retry never creates two campaigns.
Scope: campaigns:write
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Internal campaign name (1–255 chars). |
campaign_type |
string | yes | One of price_drop, back_in_stock, low_stock, reminder, recommendation, custom (≤50 chars). |
subject |
string | no | Email subject line (≤500 chars). |
body_html |
string | no | HTML body. |
body_text |
string | no | Plain-text body. |
audience_filter |
object | no | Segment selector, e.g. { "shopify_product_id": 8042113990741 }. |
Unknown fields are rejected (422 unprocessable).
curl "https://listiq.kernvellabs.com/api/public/v1/campaigns" \
-X POST \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0c2a91-0b3e-4d2a-bb9c-7e1d2f3a4b5c" \
-d '{
"name": "High-intent reminder",
"campaign_type": "reminder",
"subject": "Still thinking it over?",
"audience_filter": { "shopify_product_id": 8042113990741 }
}'
{
"object": "campaign",
"id": "camp_c93de014",
"name": "High-intent reminder",
"subject": "Still thinking it over?",
"campaign_type": "reminder",
"status": "draft",
"total_sent": 0,
"total_opened": 0,
"total_clicked": 0,
"total_failed": 0,
"ai_generated": false,
"scheduled_at": null,
"sent_at": null,
"completed_at": null,
"created_at": "2026-06-24T10:00:00Z"
}
Notable errors: 422 unprocessable (unknown campaign_type, or an unknown field in the body), 409 idempotency_conflict (key reused with a different body), 403 feature_not_in_plan (creating campaigns by API is a Pro/Business capability).
POST /campaigns/{campaign_id}/send
Send (execute) a campaign. Only campaigns in draft, scheduled, or paused status can be sent. Returns 200 with the campaign's updated stats. Idempotent — send an Idempotency-Key so a retried send never dispatches twice.
Scope: campaigns:write
| Param | In | Type | Description |
|---|---|---|---|
campaign_id |
path | string | The campaign id (camp_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/campaigns/camp_c93de014/send" \
-X POST \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx" \
-H "Idempotency-Key: 7a2b1c90-44dd-4e3f-9b22-0c9e8f7a6b5d"
{
"object": "campaign",
"id": "camp_c93de014",
"name": "High-intent reminder",
"subject": "Still thinking it over?",
"campaign_type": "reminder",
"status": "completed",
"total_sent": 1840,
"total_opened": 0,
"total_clicked": 0,
"total_failed": 0,
"ai_generated": false,
"scheduled_at": null,
"sent_at": "2026-06-24T10:00:14Z",
"completed_at": "2026-06-24T10:07:51Z",
"created_at": "2026-06-24T10:00:00Z"
}
When the campaign finishes sending it fires the campaign.sent webhook. In test mode, no email is ever delivered — the campaign is marked completed with zero recipients and only a test-mode event is emitted.
Notable errors: 404 not_found (no such campaign / not yours), 409 conflict (the campaign isn't in a sendable status, e.g. already completed), 403 feature_not_in_plan (sending campaigns by API is a Pro/Business capability).
JavaScript (fetch) example — create a draft then send it:
const create = await fetch("https://listiq.kernvellabs.com/api/public/v1/campaigns", {
method: "POST",
headers: {
Authorization: "Bearer kv_lst_live_xxxxxxxx",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
name: "Back-in-stock — overshirt",
campaign_type: "back_in_stock",
audience_filter: { shopify_product_id: 8042113990741 },
}),
});
const campaign = await create.json();
console.log(campaign.id, campaign.status); // "draft"
const sent = await fetch(
`https://listiq.kernvellabs.com/api/public/v1/campaigns/${campaign.id}/send`,
{
method: "POST",
headers: {
Authorization: "Bearer kv_lst_live_xxxxxxxx",
"Idempotency-Key": crypto.randomUUID(),
},
}
);
console.log((await sent.json()).status); // "completed"
Demand Insights
ListIQ analyzes wishlist demand across stores and turns it into periodic demand-pulse reports — your store versus peers, with the analysis already k-anonymized at compute time. Each report carries a free-form report body plus the size of the dataset it was computed over. Pull them into your own BI tool.
GET /demand_insights
List your store's demand-pulse reports, newest first.
Scope: demand_insights:read
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25. |
cursor |
query | string | next_cursor from a previous page. |
curl "https://listiq.kernvellabs.com/api/public/v1/demand_insights?limit=2" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "demand_insight",
"id": "di_2c4f9a01",
"report_window_days": 7,
"total_wishlists_analyzed": 142,
"total_stores_analyzed": 18,
"report": {
"trending_categories": ["Outerwear", "Knitwear"],
"summary": "Outerwear demand is rising across peer stores this week."
},
"generated_at": "2026-06-23T02:30:00Z",
"expires_at": "2026-06-30T02:30:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Notable errors: 403 insufficient_scope (missing demand_insights:read), 403 feature_not_in_plan (demand insights aren't in your plan).
GET /demand_insights/{insight_id}
Fetch a single demand-pulse report, including its full report body.
Scope: demand_insights:read
| Param | In | Type | Description |
|---|---|---|---|
insight_id |
path | string | The report id (di_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/demand_insights/di_2c4f9a01" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "demand_insight",
"id": "di_2c4f9a01",
"report_window_days": 7,
"total_wishlists_analyzed": 142,
"total_stores_analyzed": 18,
"report": {
"trending_categories": ["Outerwear", "Knitwear"],
"summary": "Outerwear demand is rising across peer stores this week."
},
"generated_at": "2026-06-23T02:30:00Z",
"expires_at": "2026-06-30T02:30:00Z"
}
The report object is passed through as computed — its keys evolve as the analysis improves, so treat it as a free-form JSON document rather than a fixed schema.
Notable errors: 404 not_found (no such report / not yours — a fresh store with little wishlist activity may have none yet), 403 feature_not_in_plan (demand insights aren't in your plan).
JavaScript (fetch) example — pull the latest report:
const res = await fetch(
"https://listiq.kernvellabs.com/api/public/v1/demand_insights?limit=1",
{ headers: { Authorization: "Bearer kv_lst_live_xxxxxxxx" } }
);
const { data } = await res.json();
if (!data.length) { console.log("No report yet — needs more wishlist activity"); }
else { console.log(data[0].report.summary); }
Account
Read your store's plan, status, and API capabilities — useful to check which scopes and limits your key can use before you build against them.
GET /account
Return your store's plan, subscription status, and API access block.
Scope: account:read
curl "https://listiq.kernvellabs.com/api/public/v1/account" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "account",
"id": "shop_4f1a2b3c",
"shop_domain": "demo-store.myshopify.com",
"plan": "pro",
"status": "active",
"currency": "USD",
"test_mode": false,
"api_access": {
"live_keys": true,
"max_keys": 25,
"scopes": ["account:read", "webhooks:manage", "wishlists:read", "wishlist_items:read", "wishlist_items:write", "subscribers:read", "campaigns:read", "campaigns:write", "demand_insights:read", "customers:read"],
"rate_per_min": 300,
"webhook_endpoints": 5
}
}
GET /account/usage
Return current usage and your live rate-limit snapshot.
Scope: account:read
curl "https://listiq.kernvellabs.com/api/public/v1/account/usage" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "usage",
"plan": "pro",
"wishlist_count": 1284,
"campaign_count": 37,
"actions_this_month": 4120,
"rate_limit": 300,
"rate_limit_remaining": 298
}
Notable errors: 403 insufficient_scope (missing account:read).
JavaScript (fetch) example — confirm a scope is available before using it:
const res = await fetch("https://listiq.kernvellabs.com/api/public/v1/account", {
headers: { Authorization: "Bearer kv_lst_live_xxxxxxxx" },
});
const account = await res.json();
if (!account.api_access.scopes.includes("customers:read")) {
console.warn("This plan can't read customer PII — upgrade to enable customers:read");
}
Webhooks
Subscribe to ListIQ events so your systems hear about wishlist activity and sent campaigns the moment they happen — no polling. See Webhook events for the catalog and a sample payload.
POST /webhooks
Create a webhook subscription. New subscriptions return 201. The response includes the signing secret exactly once — store it; you need it to verify deliveries.
Exact retries with the same URL, live/test environment, and event set return 200 with 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.
Scope: webhooks:manage
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | yes | Your HTTPS endpoint. Must be public — private/loopback/metadata IPs are rejected (SSRF guard). |
events |
array | yes | One or more event types from the catalog. |
description |
string | no | A label for your reference (≤255 chars). |
Unknown fields are rejected (422 unprocessable).
curl "https://listiq.kernvellabs.com/api/public/v1/webhooks" \
-X POST \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/hooks/listiq",
"events": ["wishlist.item_added", "campaign.sent"],
"description": "CRM + planning sync"
}'
{
"object": "webhook_subscription",
"id": "whk_6b2e9af1",
"url": "https://yourapp.com/hooks/listiq",
"events": ["wishlist.item_added", "campaign.sent"],
"description": "CRM + planning sync",
"env": "live",
"is_active": true,
"signing_secret": "whsec_3kQ9...shown_once...8Zx2",
"created_at": "2026-06-24T10:00:00Z"
}
Notable errors: 400 invalid_request (non-HTTPS or private-IP URL), 422 invalid_request (unknown event type in events), 403 feature_not_in_plan (webhooks not in your plan, or you're at your plan's webhook_endpoints cap).
GET /webhooks
List your webhook subscriptions (the signing secret is never returned again).
Scope: webhooks:manage
curl "https://listiq.kernvellabs.com/api/public/v1/webhooks" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "webhook_subscription",
"id": "whk_6b2e9af1",
"url": "https://yourapp.com/hooks/listiq",
"events": ["wishlist.item_added", "campaign.sent"],
"description": "CRM + planning sync",
"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 webhook subscription (no signing secret).
Scope: webhooks:manage
| Param | In | Type | Description |
|---|---|---|---|
webhook_id |
path | string | The subscription id (whk_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/webhooks/whk_6b2e9af1" \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{
"object": "webhook_subscription",
"id": "whk_6b2e9af1",
"url": "https://yourapp.com/hooks/listiq",
"events": ["wishlist.item_added", "campaign.sent"],
"description": "CRM + planning sync",
"env": "live",
"is_active": true,
"created_at": "2026-06-24T10:00:00Z"
}
Notable errors: 404 not_found (no such subscription / not yours).
DELETE /webhooks/{webhook_id}
Delete a subscription. We stop delivering to it immediately. Returns 200.
Scope: webhooks:manage
| Param | In | Type | Description |
|---|---|---|---|
webhook_id |
path | string | The subscription id (whk_...). |
curl "https://listiq.kernvellabs.com/api/public/v1/webhooks/whk_6b2e9af1" \
-X DELETE \
-H "Authorization: Bearer kv_lst_live_xxxxxxxx"
{ "object": "webhook_subscription", "id": "whk_6b2e9af1", "deleted": true }
Notable errors: 404 not_found (no such subscription / not yours).
JavaScript (fetch) example — subscribe and capture the secret:
const res = await fetch("https://listiq.kernvellabs.com/api/public/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer kv_lst_live_xxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://yourapp.com/hooks/listiq",
events: ["campaign.sent"],
}),
});
const sub = await res.json();
// Store sub.signing_secret now — it is never shown again.
await saveSecret(sub.id, sub.signing_secret);
Webhook events
ListIQ emits these event types. Subscribe to any of them on a POST /webhooks subscription. Every delivery is an event envelope signed with X-Kernvel-Signature (verify it — see the intro).
| Event | Fires when | data payload |
|---|---|---|
wishlist.item_added |
A shopper adds a product to a wishlist | The wishlist_item object |
campaign.sent |
A campaign finishes sending | The campaign object (with final stats) |
back_in_stock.subscribed |
A shopper opts into back-in-stock alerts | The subscriber object (masked email only) |
demand.insight_ready |
A new store demand-pulse report is generated | The demand_insight object |
Sample delivery — wishlist.item_added:
{
"id": "evt_9c2a17f0",
"type": "wishlist.item_added",
"api_version": "2026-06-24",
"created_at": "2026-06-24T10:00:00Z",
"data": {
"object": "wishlist_item",
"id": "wsi_71c4d8a0",
"wishlist_id": "wl_8fk3a1b2",
"shopify_product_id": "8042113990741",
"shopify_variant_id": "44910292828245",
"product_title": "Merino Wool Overshirt",
"product_handle": "merino-wool-overshirt",
"image_url": null,
"currency": "USD",
"price_when_added": "99.00",
"current_price": "99.00",
"is_available": true,
"priority": 4,
"created_at": "2026-06-24T10:00:00Z"
}
}
Verify the X-Kernvel-Signature: t=<unix>,v1=<hmac> header by computing HMAC-SHA256(signing_secret, "<t>.<raw-body>") and comparing in constant time; reject any delivery whose timestamp is more than 5 minutes old. Test-mode subscriptions receive only test-mode events. Deliveries are retried with backoff; a subscription that keeps failing is auto-disabled and you're emailed.