Kernvel AI Auction — API Reference
The Kernvel AI Auction API lets you (or your developer) read and manage your store's auction data programmatically — build custom dashboards, sync auctions with another system, place bids from your own front-end, or automate workflows. 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://auctionai.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 Kernvel AI Auction → API Keys (in the app's left navigation).
- 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_auc_live_xxxxxxxx (live) or kv_auc_test_xxxxxxxx (test mode). The auc segment marks them as Kernvel AI Auction keys.
Authentication
Send your key as a Bearer token on every request:
curl https://auctionai.kernvellabs.com/api/public/v1/auctions \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
(X-API-Key: kv_auc_live_xxxxxxxx works too.) Your key is tied to your store — you never pass a shop name. Unauthenticated requests get 401.
The only route that needs no key is GET /ping (a liveness check that returns no store data).
Test mode
Use a *_test_* key to build and experiment safely: test mode never sends real emails, never charges, never creates real orders or draft orders. Test keys are available on every plan, so you can build before you upgrade. Test responses include the header X-Kernvel-Test-Mode: true, and test-mode webhook subscriptions only receive test-mode events.
Permissions (scopes)
Each key only does what you allow. Scopes are resource:action, e.g. auctions:read, auctions:write. A request missing the needed scope returns 403 insufficient_scope telling you which scope to add. Grant the least your integration needs. Customer and winner contact details sit behind their own PII scopes and are only available on higher plans.
The scopes Kernvel AI Auction recognises:
| Scope | What it allows |
|---|---|
account:read |
Read plan, usage and limits for the store. |
auctions:read |
Read auctions. |
auctions:write |
Create and update auctions. |
auctions:manage |
Cancel auctions and other sensitive auction ops. |
bids:read |
Read bids on the store's auctions. |
bids:write |
Place bids. |
bidders:manage |
Ban and unban bidders. |
products:read |
Read products available for auction. |
webhooks:manage |
Create, list and delete webhook subscriptions. |
winners:read |
Read auction winners (contact details are PII). |
customers:read |
Read customer contact details (PII). |
PII scopes (winners:read, customers:read) are never granted by default — you must explicitly check the box when minting the key, and they're only available on top plans.
Plans
API access is part of your Kernvel AI Auction plan, enforced two ways: which scopes a plan can grant, and how many requests per minute it allows.
| Plan | Live keys | Scopes available | Webhooks | Rate limit |
|---|---|---|---|---|
| Free | Test keys only | read-only (account, auctions, bids, products) |
— | 60 / min |
| Starter | Yes | read-only (+ winners:read) |
— | 120 / min |
| Pro | Yes | full read + write + manage + webhooks + PII | up to 5 endpoints | 300 / min |
| Enterprise | Yes | full read + write + manage + webhooks + PII | up to 20 endpoints | 600 / min |
A key can never hold a scope its plan doesn't grant — the effective scope set is always what you requested ∩ what the plan allows. Ask for an out-of-plan scope (e.g. auctions:write on Starter) and you'll get 403 feature_not_in_plan with an upgrade pointer. Live API access also requires an active subscription; a cancelled or expired plan loses live keys (test keys keep working).
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:
Cursors are opaque — pass back the exactcurl "https://auctionai.kernvellabs.com/api/public/v1/auctions?limit=50&cursor=WyJudWxsIiw0Ml0" \ -H "Authorization: Bearer kv_auc_live_xxxxxxxx"next_cursorstring we return; don't construct your own. A tampered cursor returns400 invalid_request. - Filtering: the auctions list documents its
statusfilter (?status=active). - Idempotency (important for writes): send an
Idempotency-Key: <unique-id>header on anyPOST/PATCH/DELETE. If a request is retried (network hiccup), we return the original result instead of doing it twice — so a retried bid or auction never happens twice. Required on money/state actions like placing a bid. Replaying the same key with a different body returns409 idempotency_conflict. Stored results live for 24 hours.
Responses
- A single item is returned directly; lists are wrapped:
{ "object": "list", "data": [ { "id": "auc_8123", "object": "auction", "...": "..." } ], "has_more": true, "next_cursor": "WyJudWxsIiw0Ml0" } - Every object carries an
"object"type tag and a stringidwith a type prefix (auc_,bid_,prod_,whk_,shop_). - Money is a decimal string with an explicit
currency("current_price": "120.00", "currency": "USD") — never a float. Times are UTC ISO-8601 with a trailingZ(2026-06-23T10:00:00Z). - Every response includes
X-Request-Id(quote it in support tickets), theX-Kernvel-Api-Versionheader, and rate-limit headers (below).
Rate limits
Each key has a per-minute limit that scales with your plan (60 / 120 / 300 / 600 by tier — see Plans). Every response includes:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 298
X-RateLimit-Reset: 1750671600
If you exceed it you get 429 rate_limited 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 'auctions:write'.",
"errors": [ { "field": "scope", "message": "Required: auctions:write" } ],
"request_id": "req_8fk3a1" }
The codes you may see:
| HTTP | code |
When it fires |
|---|---|---|
| 400 | invalid_request |
Malformed request — bad cursor, unknown filter, bad webhook URL. |
| 400 | validation_failed |
A field failed validation; see errors[]. |
| 401 | invalid_api_key |
Key missing, malformed, revoked, or for another app. |
| 401 | expired_api_key |
The key's expiry has passed. |
| 403 | insufficient_scope |
The key lacks the scope this route needs. |
| 403 | feature_not_in_plan |
The scope/feature isn't in your plan, or your subscription isn't active. |
| 404 | not_found |
No such resource — or it belongs to another store. |
| 409 | idempotency_conflict |
Same Idempotency-Key, different body. |
| 409 | conflict |
The resource is in a conflicting state (e.g. bidding on an ended auction). |
| 422 | unprocessable |
The request was understood but couldn't be processed (e.g. invalid auction parameters). |
| 429 | rate_limited |
Per-minute limit exceeded; wait Retry-After seconds. |
| 500 | internal_error |
Something went wrong on our side (no detail leaked; quote the request_id). |
| 503 | service_unavailable |
The public API is temporarily disabled. |
A cross-store reference returns
404, not403— we never confirm whether another store'sauc_…exists.
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, available on Pro and Enterprise):
The response includes a signing secret (curl https://auctionai.kernvellabs.com/api/public/v1/webhooks -X POST \ -H "Authorization: Bearer kv_auc_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/hooks/auctionai", "events": ["auction.created"] }'whsec_…, shown once). - We POST an event envelope:
{ "id": "evt_8fk3", "type": "auction.created", "api_version": "2026-06-23", "created_at": "2026-06-23T10: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.
See Webhook events for the full list and a verification snippet. We retry failed deliveries with backoff (1m, 5m, 30m, 2h, 6h) and auto-disable an endpoint after 15 consecutive failures.
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 (currently 2026-06-23).
Tools
- OpenAPI spec:
https://auctionai.kernvellabs.com/api/public/v1/openapi.json(import into your tooling). - Postman collection: download the AuctionAI collection, or export the latest one from the API Keys page.
- Liveness:
GET https://auctionai.kernvellabs.com/api/public/v1/ping(no auth).
Need help?
Email support@kernvellabs.com with the request_id from the response — that lets us find your exact call instantly.
Resources
Kernvel AI Auction exposes five resources. Auctions is the full reference resource (read + write + idempotent state changes); Bids are nested under an auction; Products, Account, and Webhooks round it out.
| Resource | Endpoints | Scopes |
|---|---|---|
| Auctions | list, get, create, update, cancel | auctions:read / auctions:write / auctions:manage |
| Bids | list, place | bids:read / bids:write |
| Products | list, get | products:read |
| Account | get, usage | account:read |
| Webhooks | create, list, get, delete | webhooks:manage |
Auctions
Your auctions — the heart of the API. Read them, create them, change them, and cancel them. Every write is idempotent and emits a webhook event.
The auction object:
{
"object": "auction",
"id": "auc_8123",
"title": "Vintage Omega Seamaster",
"status": "active",
"auction_type": "standard",
"currency": "USD",
"current_price": "320.00",
"starting_price": "100.00",
"reserve_price": "300.00",
"buy_now_price": "650.00",
"final_price": null,
"total_bids": 14,
"unique_bidders": 6,
"product_id": "prod_412",
"starts_at": "2026-06-20T09:00:00Z",
"ends_at": "2026-06-24T09:00:00Z",
"ended_at": null,
"created_at": "2026-06-19T18:30:00Z"
}
| Field | Meaning |
|---|---|
status |
One of draft, scheduled, active, ending_soon, ended (has a winner, awaiting payment), sold (paid), cancelled, failed (no bids / reserve not met). |
auction_type |
standard, proxy, popcorn, penny, dutch, or sealed. |
current_price |
The live high bid (or the starting price if no bids yet). |
final_price |
Set once the auction ends with a winner; null while live. |
total_bids / unique_bidders |
Live counters. |
product_id |
The Kernvel product this auction sells (prod_…); see Products. |
GET /auctions
List your store's auctions, newest first. Cursor-paginated.
Scope: auctions:read
| Param | In | Type | Notes |
|---|---|---|---|
status |
query | string | Filter: active, scheduled, ended, cancelled, failed, or draft. active includes ending_soon; ended includes sold. |
limit |
query | int | 1–100, default 25. |
cursor |
query | string | The next_cursor from the previous page. |
curl "https://auctionai.kernvellabs.com/api/public/v1/auctions?status=active&limit=2" \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "auction",
"id": "auc_8123",
"title": "Vintage Omega Seamaster",
"status": "active",
"auction_type": "standard",
"currency": "USD",
"current_price": "320.00",
"starting_price": "100.00",
"reserve_price": "300.00",
"buy_now_price": "650.00",
"final_price": null,
"total_bids": 14,
"unique_bidders": 6,
"product_id": "prod_412",
"starts_at": "2026-06-20T09:00:00Z",
"ends_at": "2026-06-24T09:00:00Z",
"ended_at": null,
"created_at": "2026-06-19T18:30:00Z"
}
],
"has_more": true,
"next_cursor": "WyJudWxsIiw4MTIzXQ"
}
Notable errors: 400 invalid_request (unknown status value or bad cursor), 403 insufficient_scope.
GET /auctions/{auction_id}
Fetch a single auction by id.
Scope: auctions:read
| Param | In | Type | Notes |
|---|---|---|---|
auction_id |
path | string | The auction id, e.g. auc_8123. |
curl https://auctionai.kernvellabs.com/api/public/v1/auctions/auc_8123 \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
Returns the auction object shown above.
Notable errors: 404 not_found (no such auction, or it belongs to another store).
POST /auctions
Create an auction for one of your products. Idempotent — send an Idempotency-Key so a retry never creates a duplicate.
Scope: auctions:write
| Field | Type | Required | Notes |
|---|---|---|---|
product_id |
string | yes | A Kernvel product id (prod_…) you own; see Products. |
title |
string | no | Up to 500 chars. Defaults from the product. |
description |
string | no | |
auction_type |
string | no | standard (default), proxy, popcorn, penny, dutch, sealed. |
visibility |
string | no | public (default), private, unlisted. |
scheduled_start |
string (ISO-8601) | no | When the auction should start; omit to start now. |
duration_hours |
int | no | 1–720, default 24. |
starting_price |
number | yes | ≥ 0. |
reserve_price |
number | no | The hidden minimum; must be ≥ starting_price. |
buy_now_price |
number | no | Must be > starting_price. |
min_bid_increment |
number | no | Default 1.0. |
currency |
string | no | 3-letter code, default USD. |
popcorn_enabled |
bool | no | Anti-snipe time extension. Default false. |
payment_due_hours |
int | no | 1–168, default 48. |
Unknown fields are rejected (422). Internal/cross-app fields (wishlister targeting, AI flags, liquidation provenance) are not settable via the API — drive those from the app.
curl https://auctionai.kernvellabs.com/api/public/v1/auctions -X POST \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7d8f2c1a-3b4e-4f9a-9c2d-1e2f3a4b5c6d" \
-d '{
"product_id": "prod_412",
"title": "Vintage Omega Seamaster",
"starting_price": 100,
"reserve_price": 300,
"buy_now_price": 650,
"duration_hours": 96,
"popcorn_enabled": true
}'
Responds 201 Created with the new auction object ("status": "scheduled" or "draft" until it starts).
Notable errors: 404 not_found (product not found / not yours), 422 unprocessable (invalid parameters, e.g. reserve_price below starting_price), 409 idempotency_conflict.
PATCH /auctions/{auction_id}
Update a live or scheduled auction. Idempotent.
Scope: auctions:write
| Field | Type | Notes |
|---|---|---|
title |
string | Up to 500 chars. |
description |
string | |
visibility |
string | public, private, unlisted. |
extend_hours |
int | 1–168 — push the end time out. |
buy_now_price |
number | Must be > 0. |
popcorn_enabled |
bool |
curl https://auctionai.kernvellabs.com/api/public/v1/auctions/auc_8123 -X PATCH \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2a1b9c8d-7e6f-4a3b-8c2d-9f1e0a2b3c4d" \
-d '{ "extend_hours": 12, "buy_now_price": 700 }'
Returns the updated auction object and emits auction.updated.
Notable errors: 404 not_found, 409 conflict (the auction has already ended or is otherwise in a state that can't be edited), 409 idempotency_conflict.
DELETE /auctions/{auction_id}
Cancel an auction. Idempotent. This is a state change (cancellation), not a hard delete — the auction stays readable with "status": "cancelled".
Scope: auctions:manage
| Param | In | Type | Notes |
|---|---|---|---|
auction_id |
path | string | The auction id. |
curl https://auctionai.kernvellabs.com/api/public/v1/auctions/auc_8123 -X DELETE \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx" \
-H "Idempotency-Key: 9f8e7d6c-5b4a-4938-8271-6c5d4e3f2a1b"
Returns the cancelled auction object and emits auction.cancelled.
Notable errors: 404 not_found, 409 conflict (already ended/sold and can't be cancelled).
JavaScript (fetch)
const BASE = "https://auctionai.kernvellabs.com/api/public/v1";
const KEY = process.env.AUCTIONAI_API_KEY; // kv_auc_live_...
// List active auctions
const res = await fetch(`${BASE}/auctions?status=active&limit=25`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) throw new Error(`API ${res.status}: ${(await res.json()).detail}`);
const { data, next_cursor } = await res.json();
console.log(`${data.length} active auctions`, next_cursor);
// Create an auction (idempotent)
const created = await fetch(`${BASE}/auctions`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
product_id: "prod_412",
starting_price: 100,
reserve_price: 300,
duration_hours: 96,
}),
});
console.log(await created.json());
Bids
Read an auction's bid history, or place a bid on behalf of one of your customers (handy if you run bidding from your own front-end). Bids are nested under an auction.
The bid object:
{
"object": "bid",
"id": "bid_55920",
"auction_id": "auc_8123",
"amount": "330.00",
"currency": "USD",
"status": "winning",
"source": "api",
"created_at": "2026-06-23T11:42:05Z"
}
| Field | Meaning |
|---|---|
amount |
The bid amount (decimal string). |
status |
pending, accepted, winning, outbid, won, rejected, retracted, fraud_flagged, or forfeited. |
source |
Where the bid came from — api for bids you place through this API (vs web, mobile, proxy). |
GET /auctions/{auction_id}/bids
List bids on an auction, newest first. Cursor-paginated.
Scope: bids:read
| Param | In | Type | Notes |
|---|---|---|---|
auction_id |
path | string | The auction id (auc_…). |
limit |
query | int | 1–100, default 25. |
cursor |
query | string | The next_cursor from the previous page. |
curl "https://auctionai.kernvellabs.com/api/public/v1/auctions/auc_8123/bids?limit=2" \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "bid",
"id": "bid_55920",
"auction_id": "auc_8123",
"amount": "330.00",
"currency": "USD",
"status": "winning",
"source": "api",
"created_at": "2026-06-23T11:42:05Z"
},
{
"object": "bid",
"id": "bid_55918",
"auction_id": "auc_8123",
"amount": "320.00",
"currency": "USD",
"status": "outbid",
"source": "web",
"created_at": "2026-06-23T11:30:12Z"
}
],
"has_more": true,
"next_cursor": "WyJudWxsIiw1NTkxOF0"
}
Notable errors: 404 not_found (no such auction, or not yours), 403 insufficient_scope.
POST /auctions/{auction_id}/bids
Place a bid on behalf of a customer. Idempotent — required on this money/state-moving write, so always send an Idempotency-Key.
Scope: bids:write
| Field | Type | Required | Notes |
|---|---|---|---|
amount |
number | yes | Must be > 0 and meet the auction's current price + minimum increment. |
customer_id |
string | yes | The Shopify customer id the bid is attributed to. |
customer_email |
string | no | Used to create the bidder record if this customer is new to the app. |
customer_name |
string | no | Same — for first-time bidders. |
The bid is recorded with "source": "api". The customer is resolved (or created) within your store; you never pass a shop.
curl https://auctionai.kernvellabs.com/api/public/v1/auctions/auc_8123/bids -X POST \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4c3d2e1f-0a9b-4c8d-7e6f-5a4b3c2d1e0f" \
-d '{
"amount": 330,
"customer_id": "gid://shopify/Customer/1234567890",
"customer_email": "buyer@example.com",
"customer_name": "Jordan Lee"
}'
Responds 201 Created with the bid object and emits bid.placed.
Notable errors:
409 conflict— the bid couldn't be accepted: the auction has ended, the bid is below the current price + increment, or the bidder isn't allowed (e.g. an early-access window). Thedetailexplains which.404 not_found— no such auction, or the customer couldn't be resolved.409 idempotency_conflict— sameIdempotency-Key, different body.
JavaScript (fetch)
const BASE = "https://auctionai.kernvellabs.com/api/public/v1";
const KEY = process.env.AUCTIONAI_API_KEY;
async function placeBid(auctionId, amount, customerId) {
const res = await fetch(`${BASE}/auctions/${auctionId}/bids`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ amount, customer_id: customerId }),
});
const body = await res.json();
if (res.status === 409) {
// e.g. outbid or auction ended — show body.detail to the shopper
throw new Error(`Bid rejected: ${body.detail}`);
}
if (!res.ok) throw new Error(`API ${res.status}: ${body.detail}`);
return body; // the bid object
}
const bid = await placeBid("auc_8123", 330, "gid://shopify/Customer/1234567890");
console.log(bid.status); // "winning"
Products
The products in your store that are available to auction (synced from Shopify). Read-only over the API — you create auctions from these products with POST /auctions.
The product object:
{
"object": "product",
"id": "prod_412",
"title": "Vintage Omega Seamaster",
"description": "1968 stainless steel, recently serviced.",
"shopify_product_id": "8472910011223",
"sku": "OMG-SEA-68",
"condition": "good",
"currency": "USD",
"original_price": "499.00",
"image_url": "https://cdn.shopify.com/.../seamaster.jpg",
"created_at": "2026-06-10T14:00:00Z"
}
| Field | Meaning |
|---|---|
id |
The Kernvel product id (prod_…) — pass this as product_id when creating an auction. |
shopify_product_id |
The underlying Shopify product id. |
condition |
new, like_new, good, fair, or poor. |
original_price |
The product's reference price (decimal string), if set. |
image_url |
The primary image. |
GET /products
List your products, newest first. Cursor-paginated.
Scope: products:read
| Param | In | Type | Notes |
|---|---|---|---|
limit |
query | int | 1–100, default 25. |
cursor |
query | string | The next_cursor from the previous page. |
curl "https://auctionai.kernvellabs.com/api/public/v1/products?limit=2" \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "product",
"id": "prod_412",
"title": "Vintage Omega Seamaster",
"description": "1968 stainless steel, recently serviced.",
"shopify_product_id": "8472910011223",
"sku": "OMG-SEA-68",
"condition": "good",
"currency": "USD",
"original_price": "499.00",
"image_url": "https://cdn.shopify.com/.../seamaster.jpg",
"created_at": "2026-06-10T14:00:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Notable errors: 403 insufficient_scope, 400 invalid_request (bad cursor).
GET /products/{product_id}
Fetch a single product by id.
Scope: products:read
| Param | In | Type | Notes |
|---|---|---|---|
product_id |
path | string | The product id, e.g. prod_412. |
curl https://auctionai.kernvellabs.com/api/public/v1/products/prod_412 \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
Returns the product object shown above.
Notable errors: 404 not_found (no such product, or it belongs to another store).
JavaScript (fetch)
const BASE = "https://auctionai.kernvellabs.com/api/public/v1";
const KEY = process.env.AUCTIONAI_API_KEY;
// Page through every product
let cursor = null, all = [];
do {
const url = new URL(`${BASE}/products`);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
const page = await res.json();
all.push(...page.data);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
console.log(`${all.length} products`);
Account
Read your store's plan, status, and API capabilities — useful for showing your own usage UI or checking limits before a batch job.
GET /account
The store's plan and what its API access allows.
Scope: account:read
curl https://auctionai.kernvellabs.com/api/public/v1/account \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{
"object": "account",
"id": "shop_91",
"shop_domain": "your-store.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",
"auctions:read", "auctions:write", "auctions:manage",
"bids:read", "bids:write", "bidders:manage",
"winners:read", "products:read", "customers:read"
],
"rate_per_min": 300,
"webhook_endpoints": 5
}
}
| Field | Meaning |
|---|---|
plan |
free, starter, pro, or enterprise. |
subscription_status |
active, pending, cancelled, or expired. Live keys only work while active. |
test_mode |
Whether the key you called with is a test key. |
api_access.scopes |
The scopes grantable on your plan. |
Notable errors: 403 insufficient_scope (key lacks account:read).
GET /account/usage
A snapshot of your auction usage and your current rate-limit window.
Scope: account:read
curl https://auctionai.kernvellabs.com/api/public/v1/account/usage \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{
"object": "usage",
"plan": "pro",
"active_auctions": 37,
"auction_limit": 100,
"rate_limit": 300,
"rate_limit_remaining": 298
}
| Field | Meaning |
|---|---|
active_auctions / auction_limit |
How many active auctions you have vs your plan's cap. |
rate_limit / rate_limit_remaining |
Your per-minute limit and what's left in this window (also in the X-RateLimit-* headers). |
JavaScript (fetch)
const BASE = "https://auctionai.kernvellabs.com/api/public/v1";
const KEY = process.env.AUCTIONAI_API_KEY;
const res = await fetch(`${BASE}/account/usage`, {
headers: { Authorization: `Bearer ${KEY}` },
});
const usage = await res.json();
if (usage.active_auctions >= usage.auction_limit) {
console.warn("At your auction limit — upgrade before creating more.");
}
Webhooks
Manage the endpoints we POST events to (so you don't have to poll). Available on Pro and Enterprise plans. See Webhook events for the event catalog and how to verify signatures.
The subscription object:
{
"object": "webhook_subscription",
"id": "whk_7",
"url": "https://yourapp.com/hooks/auctionai",
"events": ["auction.created", "bid.placed"],
"description": "Prod listener",
"env": "live",
"is_active": true,
"created_at": "2026-06-23T10:00:00Z"
}
The signing secret is only returned once, on create — it's never shown again in list/get responses.
POST /webhooks
Create a subscription. Returns 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.
Scope: webhooks:manage
| Field | Type | Required | Notes |
|---|---|---|---|
url |
string | yes | Must be https and resolve to a public IP (private/loopback/metadata addresses are rejected). |
events |
string[] | yes | At least one event from the catalog. Unknown events are rejected. |
description |
string | no | Up to 255 chars. |
curl https://auctionai.kernvellabs.com/api/public/v1/webhooks -X POST \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/hooks/auctionai",
"events": ["auction.created", "bid.placed"],
"description": "Prod listener"
}'
{
"object": "webhook_subscription",
"id": "whk_7",
"url": "https://yourapp.com/hooks/auctionai",
"events": ["auction.created", "bid.placed"],
"description": "Prod listener",
"env": "live",
"is_active": true,
"created_at": "2026-06-23T10:00:00Z",
"signing_secret": "whsec_Hk3...redacted...9aZ"
}
The subscription's env matches the key you created it with — a live key makes a live subscription (receiving live events), a test key makes a test subscription.
Notable errors:
400 invalid_request— the URL isn't https or resolves to a private/reserved IP.422 invalid_request—eventscontains an unknown event type.403 feature_not_in_plan— webhooks aren't in your plan, or you've hit your plan's endpoint cap (5 on Pro, 20 on Enterprise).
GET /webhooks
List your subscriptions (no secrets).
Scope: webhooks:manage
curl https://auctionai.kernvellabs.com/api/public/v1/webhooks \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"object": "webhook_subscription",
"id": "whk_7",
"url": "https://yourapp.com/hooks/auctionai",
"events": ["auction.created", "bid.placed"],
"description": "Prod listener",
"env": "live",
"is_active": true,
"created_at": "2026-06-23T10:00:00Z"
}
],
"has_more": false,
"next_cursor": null
}
GET /webhooks/{webhook_id}
Fetch one subscription (no secret).
Scope: webhooks:manage
curl https://auctionai.kernvellabs.com/api/public/v1/webhooks/whk_7 \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
Returns the subscription object (without signing_secret).
Notable errors: 404 not_found.
DELETE /webhooks/{webhook_id}
Delete a subscription. We stop sending it events immediately.
Scope: webhooks:manage
curl https://auctionai.kernvellabs.com/api/public/v1/webhooks/whk_7 -X DELETE \
-H "Authorization: Bearer kv_auc_live_xxxxxxxx"
{ "object": "webhook_subscription", "id": "whk_7", "deleted": true }
Notable errors: 404 not_found.
JavaScript (fetch)
const BASE = "https://auctionai.kernvellabs.com/api/public/v1";
const KEY = process.env.AUCTIONAI_API_KEY;
const res = await fetch(`${BASE}/webhooks`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://yourapp.com/hooks/auctionai",
events: ["auction.created", "bid.placed"],
}),
});
const sub = await res.json();
// Store sub.signing_secret NOW — it is never returned again.
console.log("Save this secret:", sub.signing_secret);
Webhook events
Subscribe to any of these on a webhook subscription. Each delivery is the standard envelope — id, type, api_version, created_at, and a data object carrying the relevant resource.
| Event | Fires when | data |
|---|---|---|
auction.created |
An auction is created (including via POST /auctions). | auction object |
auction.updated |
An auction is edited. | auction object |
auction.cancelled |
An auction is cancelled. | auction object |
auction.started |
A scheduled auction goes live (its start time is reached). | auction object |
auction.ended |
An auction reaches its end — fires for every close, whether it sold, got no bids, or missed reserve. | auction object |
auction.sold |
An auction ends with a winning bid above reserve (a sale was made). | auction object |
auction.won |
An auction is won — carries the winning bidder and final price. | auction object |
bid.placed |
A bid is accepted (including via the API). | bid object |
bid.outbid |
A previous high bidder is displaced by a higher bid. Fires once per displaced bidder, carrying their now-outbid bid. | bid object |
payment.captured |
A won auction's payment is captured (the order is paid / marked sold). | auction object |
These are the event types you can subscribe to today. Creation/edit/cancel and
bid.placedare emitted on the public API write path; the lifecycle events (auction.started/auction.ended/auction.sold/auction.won/bid.outbid/payment.captured) are emitted server-side as auctions run, so you get notified without polling.
Delivery envelope
{
"id": "evt_3f1c9a2b7d4e6f80a1b2c3d4",
"type": "auction.created",
"api_version": "2026-06-23",
"created_at": "2026-06-24T09:00:01Z",
"data": {
"object": "auction",
"id": "auc_8123",
"title": "Vintage Omega Seamaster",
"status": "scheduled",
"auction_type": "standard",
"currency": "USD",
"current_price": "330.00",
"starting_price": "100.00",
"reserve_price": "300.00",
"buy_now_price": "650.00",
"final_price": null,
"total_bids": 15,
"unique_bidders": 7,
"product_id": "prod_412",
"starts_at": "2026-06-20T09:00:00Z",
"ends_at": "2026-06-24T09:00:00Z",
"ended_at": null,
"created_at": "2026-06-19T18:30:00Z"
}
}
The bid events (bid.placed, bid.outbid) carry a bid object in data instead; every other event carries the auction object shown above.
Delivery headers
Every delivery carries:
Content-Type: application/json
X-Kernvel-Signature: t=1750671601,v1=8f3c...hex...
X-Kernvel-Event-Id: evt_3f1c9a2b7d4e6f80a1b2c3d4
X-Kernvel-Event-Type: auction.created
User-Agent: Kernvel-Webhooks/1.0
Verify the signature
The X-Kernvel-Signature header is t=<unix>,v1=<hex>, where v1 is HMAC-SHA256(signing_secret, "<t>.<raw-body>"). Verify it against the raw request body (before JSON parsing) and reject anything older than 5 minutes — that stops a captured payload from being replayed.
import crypto from "node:crypto";
function verifyKernvelWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("="))
);
const t = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 min
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// constant-time compare
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example — note express.raw() so rawBody is the unparsed bytes
app.post("/hooks/auctionai", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyKernvelWebhook(
req.body.toString("utf8"),
req.headers["x-kernvel-signature"],
process.env.AUCTIONAI_WEBHOOK_SECRET
);
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
// handle event.type / event.data ...
res.sendStatus(200); // 2xx = delivered; anything else triggers a retry
});
Reply with any 2xx to acknowledge. Non-2xx (or a timeout) is retried with backoff (1m, 5m, 30m, 2h, 6h); after 15 consecutive failures we disable the endpoint and email you.