← Back to ListIQ overview
L

Kernvel ListIQ API Reference

Read and manage your ListIQ data programmatically. REST/JSON, scoped keys, signed webhooks.

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

  1. In your Shopify admin, open ListIQ → Settings → API Keys.
  2. Click Create key, name it (e.g. "Warehouse sync"), pick the scopes (permissions) it needs, and optionally an expiry.
  3. 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 the next_cursor from 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 any POST/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.

  1. Create a subscription (scope webhooks:manage):
    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"] }'
    
    The response includes a signing secret (shown once).
  2. 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" } }
    
  3. Verify every webhook using the X-Kernvel-Signature: t=<unix>,v1=<hmac> header — compute HMAC-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 /customers resource (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.