← Back to SupportIQ overview
S

Kernvel SupportIQ API Reference

The developer API for the AI support agent — read and manage conversations, knowledge, insights, and webhooks.

SupportIQ API Reference

The SupportIQ API lets you (or your developer) read and manage your store's SupportIQ data programmatically — pull conversations into your own dashboard, sync resolved tickets to a data warehouse, post replies from another tool, keep your knowledge base in version control, or feed Voice-of-Customer insights into your BI stack. 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://supportiq.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 SupportIQ → 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_sup_live_xxxxxxxx (live) or kv_sup_test_xxxxxxxx (test mode).

Authentication

Send your key as a Bearer token on every request:

curl https://supportiq.kernvellabs.com/api/public/v1/conversations \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"

(X-API-Key: kv_sup_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 customer emails, never charges, and never lets the AI agent take a real action on a customer's order. 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. conversations:read, conversations: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.

SupportIQ's scopes:

Scope Lets a key…
conversations:read List and read conversations and their messages
conversations:write Post a customer message or an agent reply to a conversation
escalations:read List and read human-handoff escalations
escalations:write Escalate a conversation to a human
knowledge:read List and read knowledge-base articles
knowledge:write Create knowledge-base articles
insights:read Read Voice-of-Customer insight rollups
customers:read (PII) List customer contacts seen on your conversations (email, name)
account:read Read your plan, usage, and limits (universal)
webhooks:manage Create, list, and delete webhook subscriptions (universal)

Messages are part of a conversation, so they're governed by the conversations:* scopes: reading a conversation's messages needs conversations:read; posting a message needs conversations:write. Escalations have their own scopes — escalations:read to list/read handoff records, escalations:write to escalate a conversation.

Plans

API access is part of your SupportIQ plan. In general: Free = test keys only (read scopes, for sandboxing); Starter = live keys with read access; Pro and up = full read/write + webhooks + customer (PII) data. 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://supportiq.kernvellabs.com/api/public/v1/conversations?limit=50&cursor=c2Vfb..." \
      -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
    
  • Filtering/sorting: each list endpoint documents its filters (e.g. ?status=open&channel=chat). Lists are returned newest first.
  • Idempotency (important for writes): send an Idempotency-Key: <unique-id> header on any POST. If a request is retried (network hiccup), we return the original result instead of doing it twice — so a retried reply or escalation never happens twice. Recommended on every state-moving write.

Responses

  • A single item is returned directly; lists are wrapped:
    { "object": "list",
      "data": [ { "id": "conv_8fk3", "object": "conversation", "...": "..." } ],
      "has_more": true, "next_cursor": "c2Vfb..." }
    
  • Money is a string with a currency ("cost_usd": "0.04", "currency": "USD"). Times are UTC ISO-8601 (2026-06-23T10:00:00Z). IDs are stable strings with a type prefix (conv_, msg_, esc_, kb_, ins_, cus_).
  • 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). Write and AI-backed endpoints have their own lower 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 'conversations:write'.",
  "errors": [ { "field": "scope", "message": "Required: conversations: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, internal_error. The detail, errors, and request_id fields tell you what to fix and what to quote if you need support.

How writes interact with the AI agent

SupportIQ is an autonomous AI support agent, and the API respects every guardrail that protects you and your customers. Public writes create queued or human-owned items — they never bypass the agent's money-action guardrails. Specifically:

  • A reply you post through the API is recorded as a human (agent) message on the conversation — it's a teammate talking, not the AI deciding. It does not invoke the autonomous agent loop and does not trigger the agent to autonomously refund, discount, or reorder.
  • The agent's autonomous actions (refunds, discount codes, reorders) stay bound by your agent mode (suggest vs auto), your per-store and per-tier auto-refund / auto-discount caps, the agent's confidence gate, and your escalation triggers — none of which the API can override. The API has no endpoint that forces a money action.
  • Escalating via the API creates a human-handoff record for a teammate to claim — it queues a human, it doesn't make the AI act.
  • In test mode, no real customer email is sent and no real action touches an order.

In short: the API lets you read everything and add human input safely. The money-moving decisions remain governed by the same caps, confidence gate, and approval rules as the in-app agent.

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://supportiq.kernvellabs.com/api/public/v1/webhooks -X POST \
      -H "Authorization: Bearer kv_sup_live_xxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://yourapp.com/hooks/supportiq", "events": ["conversation.escalated"] }'
    
    The response includes a signing secret (shown once).
  2. We POST an event envelope:
    { "id": "evt_8fk3", "type": "conversation.escalated", "api_version": "2026-06-23",
      "created_at": "2026-06-23T10: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.

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://supportiq.kernvellabs.com/api/public/v1/openapi.json (import into your tooling).
  • Postman collection: download the SupportIQ 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

The SupportIQ surface is built around the support conversation. You read conversations and their messages, read and create escalations (human-handoffs), manage your knowledge base that grounds the agent's answers, read Voice-of-Customer insights, and — with the PII scope — list the customer contacts seen on your conversations. Account and Webhooks round it out.

The full resource list:

  • Conversations — GET /conversations, GET /conversations/{id}
  • Messages — GET /conversations/{id}/messages, POST /conversations/{id}/messages
  • Escalations — GET /escalations, GET /escalations/{id}, POST /conversations/{id}/escalate
  • Knowledge Base — GET /knowledge, GET /knowledge/{id}, POST /knowledge
  • Insights — GET /insights, GET /insights/{id}
  • Customers — GET /customers
  • Account — GET /account, GET /account/usage
  • Webhooks — POST /webhooks, GET /webhooks, GET /webhooks/{id}, DELETE /webhooks/{id}

Conversations

A conversation is one support thread between a customer and the AI agent (plus any human teammates). It's the unit SupportIQ resolves. Each conversation carries an honest resolution label, a sentiment/topic, and a stable public handle.

Conversation object fields

Field Type Notes
id string conv_…
object string "conversation"
public_handle string The conversation's stable public id (the same handle the app shows)
channel string chat or email
status string open, ai_handling, needs_human, human_handling, resolved, closed
resolution string | null Honesty label: auto_resolved, answered, escalated, pending
subject string | null Thread subject (set for email; derived from theme/topic for chat)
order_ref string | null The order this thread is about (numeric id or GID)
escalated boolean Whether a human-handoff exists
sentiment string | null e.g. positive, neutral, negative
topic string | null Single-label topic
locale string | null e.g. en
message_count integer Number of messages in the thread
last_message_at string | null Last activity (UTC)
resolved_at string | null When it was resolved (UTC)
created_at string UTC

GET /conversations/{id} returns the same fields plus an embedded messages array (the full thread). The list endpoint omits messages — fetch a single conversation, or use GET /conversations/{id}/messages, to read the turns.

Customer contact details (email, name) are not embedded on the conversation. To read who is behind your conversations you use the dedicated GET /customers endpoint, which is gated behind the customers:read PII scope. This is how SupportIQ keeps personal data behind its own permission.


GET /conversations

List your store's conversations, newest first. Scope: conversations:read.

Param In Type Description
limit query integer 1–100, default 25
cursor query string Opaque pagination cursor from next_cursor
status query string Filter: open, ai_handling, needs_human, human_handling, resolved, closed
channel query string Filter: chat, email
escalated query boolean true to return only escalated threads
curl "https://supportiq.kernvellabs.com/api/public/v1/conversations?status=open&limit=2" \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "conv_8fk3a1",
      "object": "conversation",
      "public_handle": "c_7Q2xK9",
      "channel": "chat",
      "status": "open",
      "resolution": "pending",
      "subject": "Where is my order #1042?",
      "order_ref": "1042",
      "escalated": false,
      "sentiment": "neutral",
      "topic": "shipping",
      "locale": "en",
      "message_count": 3,
      "last_message_at": "2026-06-23T10:01:04Z",
      "resolved_at": null,
      "created_at": "2026-06-23T09:57:40Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c2Vfb2Zmc2V0OjEwNDI"
}

Notable errors: 403 insufficient_scope (needs conversations:read), 400 invalid_request (malformed cursor).


GET /conversations/{id}

Fetch one conversation, including its full message thread. Scope: conversations:read.

Param In Type Description
id path string The conv_… id
curl https://supportiq.kernvellabs.com/api/public/v1/conversations/conv_8fk3a1 \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "id": "conv_8fk3a1",
  "object": "conversation",
  "public_handle": "c_7Q2xK9",
  "channel": "chat",
  "status": "open",
  "resolution": "pending",
  "subject": "Where is my order #1042?",
  "order_ref": "1042",
  "escalated": false,
  "sentiment": "neutral",
  "topic": "shipping",
  "locale": "en",
  "message_count": 2,
  "last_message_at": "2026-06-23T09:58:11Z",
  "resolved_at": null,
  "created_at": "2026-06-23T09:57:40Z",
  "messages": [
    {
      "id": "msg_5a1b",
      "object": "message",
      "conversation_id": "conv_8fk3a1",
      "role": "customer",
      "content": "Where is my order #1042?",
      "resolution": null,
      "confidence": null,
      "citations": [],
      "actions": [],
      "created_at": "2026-06-23T09:57:40Z"
    },
    {
      "id": "msg_5a1c",
      "object": "message",
      "conversation_id": "conv_8fk3a1",
      "role": "assistant",
      "content": "Your order #1042 shipped yesterday and is out for delivery today.",
      "resolution": "answered",
      "confidence": 0.92,
      "citations": ["Shipping policy", "Order #1042 status"],
      "actions": [{ "type": "check_order_status", "status": "executed" }],
      "created_at": "2026-06-23T09:58:11Z"
    }
  ]
}

Notable errors: 404 not_found (unknown id, or a conversation that isn't yours — we return 404, never 403, so we don't reveal whether another store's id exists), 403 insufficient_scope.


JavaScript example — list open conversations

const res = await fetch(
  "https://supportiq.kernvellabs.com/api/public/v1/conversations?status=open&limit=50",
  { headers: { Authorization: "Bearer kv_sup_live_xxxxxxxx" } }
);
if (!res.ok) throw new Error(`SupportIQ ${res.status}: ${await res.text()}`);
const { data, next_cursor } = await res.json();
console.log(`${data.length} open conversations`, next_cursor);

Messages

Messages are the turns inside a conversation. Each has a role — customer, assistant (the AI), agent (a human teammate), or system. Assistant turns also carry the honesty signals the agent attaches to its answers: a confidence score, the citations it grounded the answer in, and the actions it took or proposed.

Message object fields

Field Type Notes
id string msg_…
object string "message"
conversation_id string conv_…
role string customer, assistant, agent, system
content string The message text
resolution string | null On assistant turns: the resolution label for this answer
confidence number | null 0–1 confidence (assistant turns)
citations array Source titles/refs the answer was grounded in
actions array [{ "type": "...", "status": "..." }] — actions taken/proposed this turn
created_at string UTC

GET /conversations/{id}/messages

List a conversation's messages, newest first. Scope: conversations:read.

Param In Type Description
id path string The conv_… id
limit query integer 1–100, default 25
cursor query string Pagination cursor
curl https://supportiq.kernvellabs.com/api/public/v1/conversations/conv_8fk3a1/messages \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "msg_5a1c",
      "object": "message",
      "conversation_id": "conv_8fk3a1",
      "role": "assistant",
      "content": "Your order #1042 shipped yesterday and is out for delivery today.",
      "resolution": "answered",
      "confidence": 0.92,
      "citations": ["Shipping policy", "Order #1042 status"],
      "actions": [{ "type": "check_order_status", "status": "executed" }],
      "created_at": "2026-06-23T09:58:11Z"
    },
    {
      "id": "msg_5a1b",
      "object": "message",
      "conversation_id": "conv_8fk3a1",
      "role": "customer",
      "content": "Where is my order #1042?",
      "resolution": null,
      "confidence": null,
      "citations": [],
      "actions": [],
      "created_at": "2026-06-23T09:57:40Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 404 not_found (unknown conversation), 403 insufficient_scope.


POST /conversations/{id}/messages

Post a reply (or simulate an inbound customer turn) onto a conversation. Scope: conversations:write. Send an Idempotency-Key (this can send a customer-facing reply, so a retry must not double-send).

Field Type Notes
content string Required. The message text (max 20000 chars)
role string agent (a human teammate reply — default) or customer (simulate an inbound customer turn). assistant/system are rejected — the AI authors its own turns and cannot be impersonated
resolve boolean When true and role is agent, mark the conversation resolved after the reply lands (mirrors the in-app reply + resolve flow). Default false

An agent reply is recorded with role: "agent" (a human) and moves the thread into human_handling. It does not invoke the autonomous AI loop or any Shopify-action tool — see How writes interact with the AI agent. In test mode, a customer-facing reply is recorded but no real email is sent.

curl https://supportiq.kernvellabs.com/api/public/v1/conversations/conv_8fk3a1/messages \
  -X POST \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9d2f3a4b-6c7e-4f80-91a2-b3c4d5e6f701" \
  -d '{ "content": "Just confirming your refund is on its way — thanks for your patience!", "resolve": true }'
{
  "id": "msg_5a1f",
  "object": "message",
  "conversation_id": "conv_8fk3a1",
  "role": "agent",
  "content": "Just confirming your refund is on its way — thanks for your patience!",
  "resolution": null,
  "confidence": null,
  "citations": [],
  "actions": [],
  "created_at": "2026-06-23T10:11:55Z"
}

Posting a message emits a message.created webhook; if resolve: true resolved the thread, a conversation.resolved webhook fires too.

Notable errors: 404 not_found, 409 conflict (the conversation is closed), 409 idempotency_conflict (same key, different body), 400 invalid_request (an unacceptable role), 422 validation_failed (empty content), 403 insufficient_scope.


JavaScript example — post a reply

const res = await fetch(
  "https://supportiq.kernvellabs.com/api/public/v1/conversations/conv_8fk3a1/messages",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer kv_sup_live_xxxxxxxx",
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ content: "Thanks for reaching out — your order ships today." }),
  }
);
const message = await res.json();
console.log(message.id, message.role); // "msg_…", "agent"

Escalations

An escalation is the human-handoff record SupportIQ creates when the agent is unsure, out of policy, or hits a hard trigger (a refund over your cap, a chargeback/legal keyword, repeated failure) — or when you flag a conversation yourself. It carries a one-paragraph briefing for the teammate who claims it. There is at most one escalation per conversation. You read escalations from the top-level /escalations collection, and you create one by escalating a conversation.

Escalation object fields

Field Type Notes
id string esc_…
object string "escalation"
conversation_id string conv_…
reason string Why it escalated (e.g. refund_over_cap, low_confidence_no_answer, legal_or_fraud_language, customer_requested_human, repeated_agent_failure, store_escalation_keyword, api_requested)
briefing string | null One-paragraph human briefing
sentiment string | null Customer sentiment at handoff
status string open, claimed, resolved
claimed_by string | null Teammate who claimed it
resolved_at string | null UTC
created_at string UTC

GET /escalations

List your store's escalations, newest first. Scope: escalations:read.

Param In Type Description
limit query integer 1–100, default 25
cursor query string Pagination cursor
status query string Filter: open, claimed, resolved
curl "https://supportiq.kernvellabs.com/api/public/v1/escalations?status=open" \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "esc_3c9d",
      "object": "escalation",
      "conversation_id": "conv_8fk3a1",
      "reason": "refund_over_cap",
      "briefing": "Customer requests a $180 refund on a defective item; this exceeds the $150 auto-refund cap. Order #1042, delivered, photo of damage attached.",
      "sentiment": "negative",
      "status": "open",
      "claimed_by": null,
      "resolved_at": null,
      "created_at": "2026-06-23T10:05:22Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 403 insufficient_scope (needs escalations:read), 400 invalid_request (malformed cursor).


GET /escalations/{id}

Fetch one escalation. Scope: escalations:read.

Param In Type Description
id path string The esc_… id
curl https://supportiq.kernvellabs.com/api/public/v1/escalations/esc_3c9d \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "id": "esc_3c9d",
  "object": "escalation",
  "conversation_id": "conv_8fk3a1",
  "reason": "refund_over_cap",
  "briefing": "Customer requests a $180 refund on a defective item; this exceeds the $150 auto-refund cap. Order #1042, delivered, photo of damage attached.",
  "sentiment": "negative",
  "status": "open",
  "claimed_by": null,
  "resolved_at": null,
  "created_at": "2026-06-23T10:05:22Z"
}

Notable errors: 404 not_found (unknown id, or an escalation that isn't yours), 403 insufficient_scope.


POST /conversations/{id}/escalate

Escalate a conversation to a human — create the handoff record so a teammate can claim it. Scope: escalations:write. Send an Idempotency-Key.

This queues a human; it does not make the AI take a money action. It sets the conversation's escalated flag and moves it to needs_human. Escalation is one-per-conversation: if the thread was already escalated, the existing escalation record is returned.

Field Type Notes
reason string Optional. A short reason (max 120 chars); defaults to api_requested
briefing string Optional. A one-paragraph briefing for the teammate (max 4000 chars); defaults to "Escalated via the public API."
curl https://supportiq.kernvellabs.com/api/public/v1/conversations/conv_8fk3a1/escalate \
  -X POST \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1f8e7d6c-5b4a-4938-8271-0a1b2c3d4e5f" \
  -d '{ "reason": "api_requested", "briefing": "VIP customer — please prioritise." }'
{
  "id": "esc_3c9e",
  "object": "escalation",
  "conversation_id": "conv_8fk3a1",
  "reason": "api_requested",
  "briefing": "VIP customer — please prioritise.",
  "sentiment": "neutral",
  "status": "open",
  "claimed_by": null,
  "resolved_at": null,
  "created_at": "2026-06-23T10:14:30Z"
}

Escalating emits a conversation.escalated webhook.

Notable errors: 409 conflict (the conversation is closed), 404 not_found, 409 idempotency_conflict, 403 insufficient_scope.


JavaScript example — escalate a conversation

const res = await fetch(
  "https://supportiq.kernvellabs.com/api/public/v1/conversations/conv_8fk3a1/escalate",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer kv_sup_live_xxxxxxxx",
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ reason: "api_requested", briefing: "Needs a manager." }),
  }
);
const escalation = await res.json();
console.log("Escalation:", escalation.id, escalation.status);

Knowledge Base

Your knowledge base is what grounds the agent's answers. Each article is a policy, page, or FAQ entry; SupportIQ chunks and embeds it so the agent can cite it. Create articles by API to keep your help content in version control or sync it from a CMS.

Knowledge article object fields

Field Type Notes
id string kb_…
object string "knowledge_article"
title string Article title
body string Full article text
source string manual, shopify_policy, shopify_page, faq
source_ref string | null Origin reference (e.g. the Shopify page handle)
is_active boolean Whether the agent may ground answers in it
created_at string UTC
updated_at string UTC

New articles are re-indexed (chunked + embedded) asynchronously — an article can take a moment to start grounding answers after a write. Your plan caps how many active articles you can store.


GET /knowledge

List your knowledge-base articles, newest first. Scope: knowledge:read.

Param In Type Description
limit query integer 1–100, default 25
cursor query string Pagination cursor
curl "https://supportiq.kernvellabs.com/api/public/v1/knowledge?limit=2" \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "kb_91a2",
      "object": "knowledge_article",
      "title": "Returns & refunds policy",
      "body": "You can return any item within 30 days...",
      "source": "manual",
      "source_ref": null,
      "is_active": true,
      "created_at": "2026-06-01T08:00:00Z",
      "updated_at": "2026-06-20T14:30:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c2Vfb2Zmc2V0Ojkx"
}

Notable errors: 403 insufficient_scope (needs knowledge:read), 400 invalid_request (malformed cursor).


GET /knowledge/{id}

Fetch one article. Scope: knowledge:read.

Param In Type Description
id path string The kb_… id
curl https://supportiq.kernvellabs.com/api/public/v1/knowledge/kb_91a2 \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "id": "kb_91a2",
  "object": "knowledge_article",
  "title": "Returns & refunds policy",
  "body": "You can return any item within 30 days...",
  "source": "manual",
  "source_ref": null,
  "is_active": true,
  "created_at": "2026-06-01T08:00:00Z",
  "updated_at": "2026-06-20T14:30:00Z"
}

Notable errors: 404 not_found, 403 insufficient_scope.


POST /knowledge

Create a knowledge-base article. Scope: knowledge:write. Send an Idempotency-Key. The new article is created with source: "manual" and queued for async re-indexing.

Field Type Notes
title string Required. Max 300 chars
body string Required. The article text
source_ref string | null Optional origin reference (max 255 chars)
is_active boolean Default true
curl https://supportiq.kernvellabs.com/api/public/v1/knowledge \
  -X POST \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c6b5a49-3829-4017-8e6f-5d4c3b2a1f09" \
  -d '{ "title": "Shipping times", "body": "Most orders ship within 1 business day and arrive in 3–5 days." }'
{
  "id": "kb_91a7",
  "object": "knowledge_article",
  "title": "Shipping times",
  "body": "Most orders ship within 1 business day and arrive in 3–5 days.",
  "source": "manual",
  "source_ref": null,
  "is_active": true,
  "created_at": "2026-06-23T10:20:00Z",
  "updated_at": "2026-06-23T10:20:00Z"
}

Notable errors: 403 feature_not_in_plan (your plan's active-article cap is reached), 422 validation_failed (empty title/body), 409 idempotency_conflict, 403 insufficient_scope.


JavaScript example — sync an article

const article = { title: "Warranty", body: "All electronics carry a 1-year warranty." };
const res = await fetch("https://supportiq.kernvellabs.com/api/public/v1/knowledge", {
  method: "POST",
  headers: {
    Authorization: "Bearer kv_sup_live_xxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify(article),
});
console.log("Created:", (await res.json()).id);

Insights

Voice-of-Customer insights are weekly rollups of what your customers are contacting you about — each topic with a count, a trend versus the prior period, a few representative quotes, and an action_hint (e.g. a restock cluster). Pull these into your BI stack to watch demand and friction over time.

Insight object fields

Field Type Notes
id string ins_…
object string "insight"
period string ISO week, YYYY-Www (e.g. 2026-W25)
topic string The topic this rollup is about
theme string | null Free-text theme
count integer Conversations in this topic this period
sentiment string | null Dominant sentiment
trend string | null up, down, flat versus the prior period
sample_quotes array A few representative customer quotes
action_hint string | null restock, content, shipping, product, policy
created_at string UTC

GET /insights

List Voice-of-Customer insight rollups, most recent period first. Scope: insights:read.

Param In Type Description
limit query integer 1–100, default 25
cursor query string Pagination cursor
period query string Filter to one ISO week (YYYY-Www)
topic query string Filter to one topic
action_hint query string Filter: restock, content, shipping, product, policy
curl "https://supportiq.kernvellabs.com/api/public/v1/insights?period=2026-W25" \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "ins_44a1",
      "object": "insight",
      "period": "2026-W25",
      "topic": "shipping",
      "theme": "delivery delays in the northeast",
      "count": 37,
      "sentiment": "negative",
      "trend": "up",
      "sample_quotes": [
        "Still hasn't arrived after 10 days",
        "Tracking hasn't updated since Tuesday"
      ],
      "action_hint": "shipping",
      "created_at": "2026-06-22T02:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 403 feature_not_in_plan (Voice-of-Customer is on higher plans), 403 insufficient_scope, 400 invalid_request (malformed cursor).


GET /insights/{id}

Fetch one insight rollup. Scope: insights:read.

Param In Type Description
id path string The ins_… id
curl https://supportiq.kernvellabs.com/api/public/v1/insights/ins_44a1 \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "id": "ins_44a1",
  "object": "insight",
  "period": "2026-W25",
  "topic": "shipping",
  "theme": "delivery delays in the northeast",
  "count": 37,
  "sentiment": "negative",
  "trend": "up",
  "sample_quotes": [
    "Still hasn't arrived after 10 days",
    "Tracking hasn't updated since Tuesday"
  ],
  "action_hint": "shipping",
  "created_at": "2026-06-22T02:00:00Z"
}

Notable errors: 404 not_found, 403 feature_not_in_plan, 403 insufficient_scope.


JavaScript example — pull this week's insights

const res = await fetch(
  "https://supportiq.kernvellabs.com/api/public/v1/insights?action_hint=restock",
  { headers: { Authorization: "Bearer kv_sup_live_xxxxxxxx" } }
);
const { data } = await res.json();
for (const i of data) console.log(`${i.period} ${i.topic}: ${i.count} (${i.trend})`);

Customers

The customer contacts seen on your store's conversations — their contact details. This is personal data (PII) and sits behind its own scope: a key needs customers:read, which is only grantable on higher plans and which the merchant must explicitly enable when creating the key. There is no other endpoint that exposes customer contact fields.

Customer object fields

Field Type Notes
id string cus_…
object string "customer"
email string | null Customer email (PII)
name string | null Customer name (PII)
shopify_customer_id string | null The Shopify customer id, when known
conversation_id string conv_… — the conversation this contact was seen on

GET /customers

List the customer contacts seen on your conversations, newest first. Scope: customers:read (PII). Only conversations that have a customer email are returned.

Param In Type Description
limit query integer 1–100, default 25
cursor query string Pagination cursor
curl https://supportiq.kernvellabs.com/api/public/v1/customers \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "cus_8fk3a1",
      "object": "customer",
      "email": "sam@example.com",
      "name": "Sam Rivera",
      "shopify_customer_id": "6627001",
      "conversation_id": "conv_8fk3a1"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 403 insufficient_scope (the key lacks customers:read), 403 feature_not_in_plan (PII access not on your plan), 400 invalid_request (malformed cursor).


JavaScript example — list customers (PII)

const res = await fetch(
  "https://supportiq.kernvellabs.com/api/public/v1/customers",
  { headers: { Authorization: "Bearer kv_sup_live_xxxxxxxx" } }
);
if (res.status === 403) {
  console.log("This key can't read customer PII — add the customers:read scope.");
} else {
  const { data } = await res.json();
  for (const c of data) console.log(c.email, c.conversation_id);
}

Account

Read your store's plan, status, and API usage — handy for showing your own dashboard "you've used N of your monthly resolutions" or checking your rate-limit headroom.


GET /account

Your store's plan, subscription status, and API capabilities. Scope: account:read.

curl https://supportiq.kernvellabs.com/api/public/v1/account \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "account",
  "id": "shop_4821",
  "shop_domain": "acme.myshopify.com",
  "plan": "pro",
  "subscription_status": "active",
  "currency": "USD",
  "test_mode": false,
  "api_access": {
    "live_keys": true,
    "max_keys": 25,
    "scopes": ["account:read", "webhooks:manage", "conversations:read", "conversations:write", "escalations:read", "escalations:write", "knowledge:read", "knowledge:write", "insights:read", "customers:read"],
    "rate_per_min": 300,
    "webhook_endpoints": 5
  }
}

Notable errors: 403 insufficient_scope (needs account:read).


GET /account/usage

Your current usage against plan limits plus a rate-limit snapshot. Scope: account:read.

curl https://supportiq.kernvellabs.com/api/public/v1/account/usage \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "usage",
  "plan": "pro",
  "year_month": "2026-06",
  "resolutions_used": 412,
  "resolutions_limit": 1000,
  "conversations": 530,
  "rate_limit": 300,
  "rate_limit_remaining": 298
}

Notable errors: 403 insufficient_scope.


JavaScript example — check remaining resolutions

const res = await fetch("https://supportiq.kernvellabs.com/api/public/v1/account/usage", {
  headers: { Authorization: "Bearer kv_sup_live_xxxxxxxx" },
});
const u = await res.json();
console.log(`${u.resolutions_used}/${u.resolutions_limit} resolutions used`);

Webhooks

Subscribe to SupportIQ events so your systems hear about new, resolved, and escalated conversations the moment they happen — no polling. See Webhook events for the catalog.

Webhook subscription object fields

Field Type Notes
id string whk_…
object string "webhook_subscription"
url string Your HTTPS endpoint
events array The event types you subscribed to
description string | null Your label for the subscription
env string live or test (matches the key that created it)
is_active boolean Whether deliveries are active
created_at string UTC
signing_secret string Only returned once, at creation — store it to verify signatures

POST /webhooks

Create a subscription. Scope: webhooks:manage. The destination URL must be HTTPS and publicly reachable (we block private/loopback addresses). The response includes the signing_secret exactly once.

New subscriptions return 201. 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.

Field Type Notes
url string Required. Your HTTPS endpoint
events array Required. One or more event types (see catalog)
description string | null Optional label, max 255 chars
curl https://supportiq.kernvellabs.com/api/public/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://yourapp.com/hooks/supportiq",
        "events": ["conversation.escalated", "conversation.resolved"],
        "description": "Ops alerts" }'
{
  "id": "whk_77c1",
  "object": "webhook_subscription",
  "url": "https://yourapp.com/hooks/supportiq",
  "events": ["conversation.escalated", "conversation.resolved"],
  "description": "Ops alerts",
  "env": "live",
  "is_active": true,
  "created_at": "2026-06-23T10:30:00Z",
  "signing_secret": "whsec_3f9a...shown-once"
}

Notable errors: 403 feature_not_in_plan (webhooks not on your plan, or your endpoint cap is reached), 400 invalid_request (non-HTTPS or private URL), 422 invalid_request (unknown event type in events), 403 insufficient_scope.


GET /webhooks

List your subscriptions (the signing secret is never returned again). Scope: webhooks:manage.

curl https://supportiq.kernvellabs.com/api/public/v1/webhooks \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "id": "whk_77c1",
      "object": "webhook_subscription",
      "url": "https://yourapp.com/hooks/supportiq",
      "events": ["conversation.escalated", "conversation.resolved"],
      "description": "Ops alerts",
      "env": "live",
      "is_active": true,
      "created_at": "2026-06-23T10:30:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

GET /webhooks/{id}

Fetch one subscription (no secret). Scope: webhooks:manage.

curl https://supportiq.kernvellabs.com/api/public/v1/webhooks/whk_77c1 \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"

Notable errors: 404 not_found, 403 insufficient_scope.


DELETE /webhooks/{id}

Delete a subscription. Scope: webhooks:manage.

curl https://supportiq.kernvellabs.com/api/public/v1/webhooks/whk_77c1 \
  -X DELETE \
  -H "Authorization: Bearer kv_sup_live_xxxxxxxx"
{ "object": "webhook_subscription", "id": "whk_77c1", "deleted": true }

Notable errors: 404 not_found, 403 insufficient_scope.


JavaScript example — verify a webhook signature

import crypto from "node:crypto";

// Express-style handler. Mount with a RAW body parser so you hash the exact bytes.
export function handleSupportIQWebhook(req, res) {
  const secret = process.env.SUPPORTIQ_WEBHOOK_SECRET; // the whsec_ shown once
  const header = req.headers["x-kernvel-signature"];   // "t=...,v1=..."
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const signed = `${parts.t}.${req.rawBody}`;
  const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");

  const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5-min window
  if (!ok || !fresh) return res.status(400).send("bad signature");

  const event = JSON.parse(req.rawBody);
  console.log(event.type, event.data.id); // e.g. "conversation.escalated", "conv_…"
  res.sendStatus(200);
}

Webhook events

SupportIQ emits these event types. Subscribe to any of them via POST /webhooks. Test-mode subscriptions receive only test-mode events.

Event Fires when data payload
conversation.created A new conversation starts (a customer opens a chat or an email arrives) A conversation object
conversation.resolved A conversation is marked resolved (by the agent or a human) A conversation object (with resolution + resolved_at)
conversation.escalated A conversation is handed off to a human An escalation object
message.created A new message is added — customer, assistant, or human A message object

The data object is the same shape you get from the matching GET endpoint, so you can treat a webhook as a push of that resource.

Sample delivery (the envelope we POST to your URL):

{
  "id": "evt_9a2f1c",
  "type": "conversation.escalated",
  "api_version": "2026-06-23",
  "created_at": "2026-06-23T10:05:25Z",
  "data": {
    "id": "esc_3c9e",
    "object": "escalation",
    "conversation_id": "conv_8fk3a1",
    "reason": "refund_over_cap",
    "briefing": "Customer requests a $180 refund; exceeds the auto-refund cap.",
    "sentiment": "negative",
    "status": "open",
    "claimed_by": null,
    "resolved_at": null,
    "created_at": "2026-06-23T10:05:22Z"
  }
}

Every delivery carries the X-Kernvel-Signature: t=<unix>,v1=<hmac> header — verify it (see the signature example above) and reject deliveries whose timestamp is older than 5 minutes.