VendorIQ — API Reference
The VendorIQ API lets you (or your developer) read and manage selected marketplace records programmatically — pull your vendor roster into an ERP, feed per-vendor order splits into a fulfillment system, reconcile commissions and payouts in your accounting tool, schedule and complete payouts from your own workflow, or subscribe to events and use GET requests to confirm current state. This reference lists the merchant-key public API. An admin screen, storefront feature, or connected shopper app is not automatically part of this 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.
Every key resolves to your store (the merchant tenant). The vendor-portal login surface is separate and is not part of this API.
Base URL
https://vendoriq.kernvellabs.com/api/public/v1
- All endpoints live under
/api/public/v1. All traffic is HTTPS only. - API version:
2026-06-23(date-based). Every response echoes it as theX-Kernvel-Api-Versionheader. v1 is the only version. - OpenAPI spec:
GET https://vendoriq.kernvellabs.com/api/public/v1/openapi.json(public routes only, no key required) — import it into your tooling to generate a client. - Human docs base:
https://docs.kernvellabs.com/api. - Availability: if merchant-key access is temporarily disabled for your store or for maintenance, authenticated resource requests return
503 service_unavailable— retry after a short back-off. The public ping and OpenAPI document remain separate.
What a public API key can access
The public API currently covers the account, vendors, vendor orders, payouts, commission rules, vendor products, invoices, and webhook subscriptions listed below. Local delivery and pickup promises, food menus, grocery picking, the money ledger and review queue, customer tracking, branded-app actions, and merchant-reviewed missing-order claims use separate authenticated app surfaces. They are not public-key endpoints under /api/public/v1; do not call a private /api/v1 or mobile-runtime route with this key. A merchant key and customers:read scope do not let an integration browse a shopper's private order timeline or approve an ownership claim. Use the VendorIQ admin or vendor portal for those workflows, and contact support if you need an integration that is not listed here.
Getting an API key
- In your Shopify admin, open VendorIQ → Settings → API Keys. Keys are minted here in the embedded admin — not through the public API itself.
- Click Create key, name it (e.g. "Accounting sync"), choose Test or Live, pick only the scopes (permissions) it needs, and optionally choose an expiry date. A chosen key expires at 23:59:59 UTC on that date. Without a chosen date, live keys default to 180 days and the server caps longer live lifetimes at its configured maximum (currently 395 days); a test key can have no scheduled expiry. The key list shows the server's final expiry in UTC.
- Copy the key immediately — the plaintext is shown exactly once, at creation. Only a hash of it is stored, so we can never show it again. Store it somewhere safe (a password manager or your server's secrets). If you lose it, revoke it and mint a new one.
Keys look like kv_ven_live_xxxxxxxx (live) or kv_ven_test_xxxxxxxx (test mode). The shape is kv_<app>_<env>_<secret>: ven is VendorIQ's app code, and the secret carries 256 bits of entropy.
Authentication
Send your key as a Bearer token on resource requests. GET /ping and GET /openapi.json need no key:
curl https://vendoriq.kernvellabs.com/api/public/v1/vendors \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
The header X-API-Key: kv_ven_live_xxxxxxxx works too. Your key is tied to your store — you never pass a shop name. Missing or bad credentials get 401 (invalid_api_key or expired_api_key).
Test mode
Use a *_test_* key to build and experiment safely:
- Scoped
GETrequests can read your store's live records, including vendor contact details. Treat a test key as a credential, not an empty sandbox. Buyer name and email still require the separatecustomers:readentitlement. - Supported test creates return deterministic synthetic ids; supported test updates preview a result without changing the live business row, and no money moves. Some state-changing routes are unavailable to test keys.
- Where your plan includes webhook subscriptions, supported test writes can emit events to test-mode subscriptions only, so you can verify delivery end-to-end.
- It keeps working during a billing lapse, so a subscription hiccup never blocks your integration work.
- Every test response carries the header
X-Kernvel-Test-Mode: true.
Test keys are available on every plan, so you can build before you upgrade.
Scopes
Each key only does what you allow. Scopes are resource:action, e.g. payouts:read, payouts:manage. A request missing the needed scope returns 403 insufficient_scope, telling you exactly which scope to add. Grant the least your integration needs.
Every scope is checked twice: your key must hold it and your plan must grant it. Missing on the key → 403 insufficient_scope; missing on the plan → 403 feature_not_in_plan.
VendorIQ's 13 scopes:
| Scope | Lets a key… |
|---|---|
account:read |
Read your plan, usage, and limits (universal) |
vendors:read |
List and read vendors |
vendors:write |
Create and update vendors |
vendor_orders:read |
List and read per-vendor order splits |
vendor_orders:write |
Create per-vendor orders and mark them fulfilled |
payouts:read |
List and read payouts |
payouts:manage |
Schedule payouts and mark them completed |
commissions:read |
List and read commission rules |
vendor_products:read |
List and read vendor products |
vendor_products:write |
Update a vendor product's approval status |
invoices:read |
List and read per-vendor invoices |
customers:read |
(PII) Include the buyer's name and email on vendor-order responses |
webhooks:manage |
Create, list, and delete webhook subscriptions (Pro and above) |
customers:readis the scope for buyer contact details on vendor orders. These fields (customer_name,customer_email) come backnullunless your key holdscustomers:readand your plan includes buyer-PII access. Other read scopes can expose vendor contact information, including a seller's name, email, and phone. Treat all API keys accordingly. Buyer-PII access, along with write and webhook scopes, is a Pro-and-up capability. On Free and Growth, keys are read-only.
API plan tiers
API access is part of your VendorIQ plan. Read scopes get you started; live keys, writes, webhooks, and buyer PII unlock as you move up.
| Tier | Live keys | Max keys | Rate / min | Webhook endpoints | Scopes |
|---|---|---|---|---|---|
| Free | No — test keys only | 2 | 60 | 0 | Read-only |
| Growth | Yes | 5 | 120 | 0 | Read-only |
| Pro | Yes | 25 | 300 | 5 | Full (read + write + manage + PII) |
| Scale | Yes | 50 | 450 | 10 | Full |
| Enterprise | Yes | 100 | 600 | 20 | Full |
- Free & Growth = "build, don't run": writes, webhooks, and buyer-PII (
customers:read) are Pro+. Free issues test keys only; Growth adds live keys but stays read-only. - Pro & up = full surface: read + write + manage + PII scopes, plus webhook endpoints.
- A live key on an inactive subscription is rejected — keep your subscription active to keep live keys working. (Test keys keep working through a billing lapse.)
- If you call an endpoint or request a scope your plan doesn't include, you get
403 feature_not_in_planwith an upgrade link.
Conventions
Consistent rules across every endpoint.
Requests
- Send JSON bodies with
Content-Type: application/json. - Pagination (cursor): list endpoints return up to
limititems (1–100, default 25). Whenhas_moreistrue, pass the returnednext_cursorback as?cursor=to page forward:curl "https://vendoriq.kernvellabs.com/api/public/v1/payouts?limit=50&cursor=c2Vfb..." \ -H "Authorization: Bearer kv_ven_live_xxxxxxxx" - Filtering: each list endpoint documents its filters (e.g.
?status=pending&vendor_id=ven_...). Lists are returned newest first. - Idempotency: send an
Idempotency-Key: <unique-id>header on vendor, vendor-order, payout, and vendor-product writes. Those routes require the key, includingPATCHand fulfillment/completion actions. If a request is retried, we return the recorded result instead of applying the same action again. Keys are honored for 24 hours; reusing one with a different body returns409 idempotency_conflict. Webhook subscription routes document their own retry behavior below.
Responses
- A single item is returned directly; lists are wrapped in a Stripe-style envelope:
{ "object": "list", "data": [ { "id": "pyt_…", "object": "payout", "...": "..." } ], "has_more": true, "next_cursor": "c2Vfb..." } - Money is a string with an explicit
currency("amount": "125.00", "currency": "USD") — never a float. Datetimes are ISO-8601 UTC with a trailingZ(2026-07-01T10:00:00Z). - IDs are stable strings with a type prefix:
ven_(vendor),vord_(vendor order),pyt_(payout),cmr_(commission rule),vpr_(vendor product),inv_(invoice),whk_(webhook subscription),shop_(account),evt_(webhook event). - Public API responses carry
X-Request-Id(quote it in support tickets) andX-Kernvel-Api-Version. Requests that reach the per-key rate limiter also carry theX-RateLimit-*headers below.
Rate limits Each key has a per-minute limit that scales with your plan (see the tier table). A request with a verified key includes:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 298
X-RateLimit-Reset: 1751364000
Exceed it and you get 429 rate_limited with a Retry-After header (seconds to wait).
How writes interact with your money
The API respects the same safety rules as the app — it can record and orchestrate payouts, but it never bypasses your control of the money. Specifically:
- Scheduling a payout (
POST /payouts) creates apendingpayout record for a vendor and period. It does not move money on its own — the actual disbursement (PayPal, card processor, or a manual transfer) is still driven from your VendorIQ admin's payout flow, sent from your own account. - Completing a payout (
POST /payouts/{id}/complete) records an externally settled payment after your own accounting confirms it. Supply the payment reference so your team can reconcile it. The endpoint does not send money; a completed payout returns its recorded state on a later request without emitting another completion event. - Money always flows from your account to your vendors. VendorIQ never holds or routes funds; the API can't make it do so.
- In test mode, no real record is created and no money moves.
Errors
Errors use the standard machine-readable RFC 9457 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 'vendors:write'.",
"errors": [ { "field": "scope", "message": "Required: vendors:write" } ],
"request_id": "req_8fk3a1..." }
The detail, errors, and request_id fields tell you what to fix and what to quote if you need support. The full catalog:
code |
HTTP | Meaning |
|---|---|---|
invalid_request |
400 | Malformed request — bad cursor, unknown filter value, or a bad path parameter |
validation_failed |
400 | The body failed validation (see errors[] for the offending fields) |
invalid_api_key |
401 | Missing, unknown, or revoked key |
expired_api_key |
401 | The key has passed its expiry |
insufficient_scope |
403 | The key doesn't hold the scope this route requires |
feature_not_in_plan |
403 or 402 | Your plan doesn't include this endpoint or scope (403), or vendor approval would exceed its seat cap (402) |
test_mode_isolated |
403 | A test key cannot perform this live state change; use a live key with the required plan and scope |
not_found |
404 | No such resource for your store (we return 404, never 403, for another store's id — so we never reveal whether it exists) |
idempotency_conflict |
409 | Same Idempotency-Key reused with a different body |
conflict |
409 | The action conflicts with current state (e.g. duplicate vendor email, fulfilling a cancelled order, completing a cancelled/failed payout) |
unprocessable |
422 | The request was well-formed but couldn't be carried out (e.g. a payout that couldn't be scheduled) |
rate_limited |
429 | Per-minute rate limit exceeded — see Retry-After |
internal_error |
500 | Something went wrong on our side. Never leaks detail — quote the request_id |
service_unavailable |
503 or 502 | The API is temporarily unavailable (503), or a product takedown could not be confirmed with Shopify (502); inspect the action before retrying |
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 (2026-06-23).
Need help?
Email support@kernvellabs.com with the request_id from the response — that lets us find your exact call instantly.
Resources
The VendorIQ surface follows your marketplace's order lifecycle: vendors onboard, their vendor products list, a Shopify order splits into per-vendor vendor orders, commission rules decide the take, payouts settle each vendor, and invoices summarize a period. You read all of it, create and update vendors and vendor orders, schedule and complete payouts, and update a product's approval status. Account, Meta, and Webhooks round it out.
Returns and disputes are managed in the app but are not exposed in the v1 API.
The full resource list (relative to /api/public/v1):
- Meta —
GET /ping,GET /openapi.json - Account —
GET /account,GET /account/usage - Vendors —
GET /vendors,GET /vendors/{id},POST /vendors,PATCH /vendors/{id} - Vendor Orders —
GET /vendor_orders,GET /vendor_orders/{id},POST /vendor_orders,POST /vendor_orders/{id}/fulfill - Payouts —
GET /payouts,GET /payouts/{id},POST /payouts,POST /payouts/{id}/complete - Commissions —
GET /commissions,GET /commissions/{id} - Vendor Products —
GET /vendor_products,GET /vendor_products/{id},PATCH /vendor_products/{id} - Invoices —
GET /invoices,GET /invoices/{id} - Webhooks —
POST /webhooks,GET /webhooks,GET /webhooks/{id},DELETE /webhooks/{id}
Meta
Unauthenticated liveness and the machine-readable spec.
GET /ping
Liveness probe. No auth required.
curl https://vendoriq.kernvellabs.com/api/public/v1/ping
{ "object": "ping", "ok": true, "app": "vendoriq", "api_version": "2026-06-23" }
GET /openapi.json
The OpenAPI spec for the public routes — no key required. Import it into your client generator or API tooling.
curl https://vendoriq.kernvellabs.com/api/public/v1/openapi.json
Account
Read your store's plan, status, and API usage — handy for showing your own dashboard or checking your rate-limit headroom.
GET /account
Your store's plan, subscription status, currency, and API capabilities. Scope: account:read.
curl https://vendoriq.kernvellabs.com/api/public/v1/account \
-H "Authorization: Bearer kv_ven_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", "vendors:read", "vendors:write", "vendor_orders:read", "vendor_orders:write", "payouts:read", "payouts:manage", "commissions:read", "vendor_products:read", "vendor_products:write", "invoices:read", "customers:read"],
"rate_per_min": 300,
"webhook_endpoints": 5
}
}
Notable errors: 403 insufficient_scope (needs account:read).
GET /account/usage
Your current vendor usage against your plan cap, plus a rate-limit snapshot. Scope: account:read.
curl https://vendoriq.kernvellabs.com/api/public/v1/account/usage \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
{
"object": "usage",
"plan": "pro",
"active_vendors": 42,
"vendor_limit": 100,
"rate_limit": 300,
"rate_limit_remaining": 298
}
vendor_limitis your plan's approved-vendor cap;-1means unlimited (Enterprise).active_vendorscounts approved, live vendors.
Notable errors: 403 insufficient_scope.
Vendors
A vendor is a supplier, brand, or seller in your marketplace. Read your roster, create vendors, and update their details or status. A vendor created via this API is a merchant-managed record — it never touches the vendor-portal login surface.
Vendor object fields
| Field | Type | Notes |
|---|---|---|
id |
string | ven_… |
object |
string | "vendor" |
business_name |
string | The vendor's business name |
email |
string | Contact email (unique per store) |
slug |
string | URL-friendly unique id within your store |
status |
string | pending, approved, suspended, rejected, inactive |
first_name / last_name |
string | null | Contact name |
phone |
string | null | Contact phone |
business_type |
string | null | e.g. individual, llc, corporation |
website |
string | null | Vendor's website |
commission_rate |
string | null | Default commission for this vendor |
commission_type |
string | null | percentage, fixed, or tiered |
total_products / total_orders |
integer | Rollup counts |
total_revenue |
string | null | Rollup revenue |
is_verified |
boolean | Whether you've verified the vendor |
created_at |
string | UTC |
GET /vendors
List your vendors, newest first. Scope: vendors:read.
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25 |
cursor |
query | string | Pagination cursor from next_cursor |
status |
query | string | Filter: pending, approved, suspended, rejected, inactive |
curl "https://vendoriq.kernvellabs.com/api/public/v1/vendors?status=approved&limit=2" \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"id": "ven_9f3a1c22",
"object": "vendor",
"business_name": "Northwind Supply Co.",
"email": "ops@northwind.example",
"slug": "northwind-supply-co",
"status": "approved",
"commission_rate": "15.00",
"commission_type": "percentage",
"total_products": 34,
"total_orders": 128,
"total_revenue": "18420.00",
"is_verified": true,
"created_at": "2026-05-04T09:12:00Z"
}
],
"has_more": true,
"next_cursor": "c2Vfb2Zmc2V0OjEyOA"
}
Notable errors: 403 insufficient_scope, 400 invalid_request (malformed cursor or unknown status).
GET /vendors/{id}
Fetch one vendor. Scope: vendors:read.
curl https://vendoriq.kernvellabs.com/api/public/v1/vendors/ven_9f3a1c22 \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
Notable errors: 404 not_found (unknown id, or a vendor that isn't yours — we return 404, never 403, so we don't reveal whether another store's id exists), 403 insufficient_scope.
POST /vendors
Create a vendor. Scope: vendors:write. Send an Idempotency-Key. Emails are unique per store; a duplicate returns 409.
| Field | Type | Notes |
|---|---|---|
business_name |
string | Required. Max 255 chars |
email |
string | Required. Valid email |
first_name / last_name |
string | null | Optional |
phone |
string | null | Optional |
business_type |
string | null | Optional |
website |
string | null | Optional |
commission_rate |
number | null | Optional, 0–100 |
commission_type |
string | null | Optional: percentage, fixed, tiered |
curl https://vendoriq.kernvellabs.com/api/public/v1/vendors \
-X POST \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9d2f3a4b-6c7e-4f80-91a2-b3c4d5e6f701" \
-d '{ "business_name": "Northwind Supply Co.", "email": "ops@northwind.example", "commission_rate": 15, "commission_type": "percentage" }'
New vendors are created with status: "pending" and emit vendor.created.
Notable errors: 409 conflict (email already exists), 400 validation_failed (missing or invalid name/email), 409 idempotency_conflict (same key, different body), 403 insufficient_scope.
PATCH /vendors/{id}
Update a vendor's details or status. Scope: vendors:write. Send an Idempotency-Key. Only the fields you send are changed.
Accepts the editable fields from create, plus status and an optional reason. Existing records may show pending or inactive, but a status change through this route can target only approved, rejected, or suspended. Moving a suspended vendor to approved reactivates it. Send reason only with rejected or suspended (up to 1,000 characters). Approval follows the same vendor-seat limit and storefront/access cleanup as the merchant admin.
curl https://vendoriq.kernvellabs.com/api/public/v1/vendors/ven_9f3a1c22 \
-X PATCH \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1f8e7d6c-5b4a-4938-8271-0a1b2c3d4e5f" \
-d '{ "status": "approved", "commission_rate": 12 }'
Emits vendor.updated.
Notable errors: 404 not_found, 400 invalid_request (unsupported transition or misplaced reason), 402 feature_not_in_plan (approval would exceed the vendor cap), 409 idempotency_conflict, 403 insufficient_scope.
JavaScript example — list approved vendors
const res = await fetch(
"https://vendoriq.kernvellabs.com/api/public/v1/vendors?status=approved&limit=50",
{ headers: { Authorization: "Bearer kv_ven_live_xxxxxxxx" } }
);
if (!res.ok) throw new Error(`VendorIQ ${res.status}: ${await res.text()}`);
const { data, next_cursor } = await res.json();
console.log(`${data.length} approved vendors`, next_cursor);
Vendor Orders
A vendor order is the per-vendor split of a Shopify order — one vendor's line items, subtotal, commission, and net earning. When VendorIQ receives an eligible Shopify order with assigned vendor products, it records each seller's share. This resource lets you read those records and, for external integrations that own their own order pipeline, create and fulfill them.
Vendor order object fields
| Field | Type | Notes |
|---|---|---|
id |
string | vord_… |
object |
string | "vendor_order" |
vendor_id |
string | ven_… |
shopify_order_id |
integer | The Shopify order id |
shopify_order_number |
string | null | The human order number (e.g. #1042) |
currency |
string | e.g. USD |
subtotal / shipping_amount / tax_amount / total_amount |
string | null | Money |
commission_amount |
string | null | Commission taken on this split |
vendor_payout_amount |
string | null | What the vendor earns from this split |
fulfillment_status |
string | unfulfilled, partial, fulfilled, cancelled |
payment_status |
string | e.g. paid |
payout_status |
string | e.g. unpaid, scheduled, paid |
tracking_number / carrier |
string | null | Shipping info |
customer_name / customer_email |
string | null | (PII) null unless the key has customers:read and the plan allows it |
created_at |
string | UTC |
GET /vendor_orders
List order splits, newest first. Scope: vendor_orders:read.
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25 |
cursor |
query | string | Pagination cursor |
status |
query | string | Filter on fulfillment: unfulfilled, partial, fulfilled, cancelled |
vendor_id |
query | string | Filter by vendor (ven_…) |
curl "https://vendoriq.kernvellabs.com/api/public/v1/vendor_orders?vendor_id=ven_9f3a1c22&status=fulfilled" \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"id": "vord_71a0",
"object": "vendor_order",
"vendor_id": "ven_9f3a1c22",
"shopify_order_id": 5500000001,
"shopify_order_number": "#1042",
"currency": "USD",
"subtotal": "120.00",
"shipping_amount": "6.00",
"tax_amount": "0.00",
"total_amount": "126.00",
"commission_amount": "18.00",
"vendor_payout_amount": "108.00",
"fulfillment_status": "fulfilled",
"payment_status": "paid",
"payout_status": "unpaid",
"tracking_number": "1Z999...",
"carrier": "UPS",
"customer_name": null,
"customer_email": null,
"created_at": "2026-06-28T14:20:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Notable errors: 403 insufficient_scope, 400 invalid_request.
GET /vendor_orders/{id}
Fetch one order split. Scope: vendor_orders:read.
Notable errors: 404 not_found, 403 insufficient_scope.
POST /vendor_orders
Synchronize the per-vendor share of an existing Shopify order that contains products assigned to this vendor. This endpoint cannot create a new Shopify order or accept caller-supplied buyer details, line items, or totals as payout authority. VendorIQ reads the Shopify order and runs its normal order-splitting flow; a test key returns a synthetic preview. Scope: vendor_orders:write. Send an Idempotency-Key.
| Field | Type | Notes |
|---|---|---|
vendor_id |
string | Required. ven_… |
shopify_order_id |
integer | Required |
shopify_order_number |
string | null | Optional |
line_items |
array | Required. One or more line items |
subtotal / total_amount |
number | Required, ≥ 0 |
shipping_amount / tax_amount / discount_amount |
number | Optional, ≥ 0 |
currency |
string | Default USD |
customer_name / customer_email |
string | null | Optional buyer info |
When this creates a new split, it starts unfulfilled and produces vendor_order.created. An existing split is returned in its current state. If your amount or currency differs from Shopify's verified split, the response includes shopify_sync.status: "reconciled" and the authoritative amounts; review those before using the result.
Notable errors: 404 not_found (vendor or no assigned products for that vendor on the verified order), 422 unprocessable (Shopify order could not be verified or synchronized), 400 invalid_request (inconsistent supplied totals), 409 idempotency_conflict, 403 insufficient_scope.
POST /vendor_orders/{id}/fulfill
Ask Shopify to fulfill a paid vendor-order share with tracking. A successful response confirms the Shopify sync; an uncertain or partial sync returns an error with vendor_order.shopify_sync so you can inspect the result before retrying. Scope: vendor_orders:write. Send an Idempotency-Key.
| Field | Type | Notes |
|---|---|---|
tracking_number |
string | null | Optional |
carrier |
string | null | Optional |
tracking_url |
string | null | Optional |
notify_customer |
boolean | Optional; defaults to true. Set false if you do not want VendorIQ to request Shopify's customer notification. |
On confirmed Shopify sync, moves fulfillment_status to fulfilled and produces vendor_order.fulfilled. A cancelled, unpaid, or already fulfilled and synced order cannot be fulfilled again (409). Shopify accepting a notification request does not prove an email reached the buyer.
curl https://vendoriq.kernvellabs.com/api/public/v1/vendor_orders/vord_71a0/fulfill \
-X POST \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c6b5a49-3829-4017-8e6f-5d4c3b2a1f09" \
-d '{ "tracking_number": "1Z999AA10123456784", "carrier": "UPS" }'
Notable errors: 404 not_found, 409 conflict (cancelled, unpaid, already fulfilled, or conflicting Shopify order state), 502 or 503 (Shopify fulfillment not confirmed or temporarily unavailable; inspect vendor_order.shopify_sync), 409 idempotency_conflict, 403 insufficient_scope.
Payouts
A payout is a single payment to a vendor covering their eligible earnings for a period. This resource lets you list and read payouts, schedule a payout, and mark one completed once your own settlement confirms.
Scheduling records the intent (a
pendingpayout); the actual money movement (PayPal / card processor / manual transfer) stays in VendorIQ's admin payout flow, sent from your own account. See How writes interact with your money.
Payout object fields
| Field | Type | Notes |
|---|---|---|
id |
string | pyt_… |
object |
string | "payout" |
vendor_id |
string | ven_… |
payout_number |
string | Human-readable payout id |
amount |
string | null | Total payout amount |
currency |
string | e.g. USD |
status |
string | pending, processing, completed, failed, cancelled |
payout_method |
string | null | e.g. paypal, stripe_connect, bank_wire |
order_count |
integer | Orders covered |
gross_total |
string | null | Gross sales before commission |
commission_total |
string | null | Commission taken |
period_start / period_end |
string | null | The period covered |
processed_at |
string | null | When submitted to the payment provider |
paid_at |
string | null | When the vendor actually received funds |
created_at |
string | UTC |
GET /payouts
List payouts, newest first. Scope: payouts:read.
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25 |
cursor |
query | string | Pagination cursor |
status |
query | string | Filter: pending, processing, completed, failed, cancelled |
vendor_id |
query | string | Filter by vendor (ven_…) |
curl "https://vendoriq.kernvellabs.com/api/public/v1/payouts?status=completed&limit=2" \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"id": "pyt_2c9d",
"object": "payout",
"vendor_id": "ven_9f3a1c22",
"payout_number": "NSC-2026-06-30-9F3A1C22",
"amount": "108.00",
"currency": "USD",
"status": "completed",
"payout_method": "paypal",
"order_count": 1,
"gross_total": "120.00",
"commission_total": "18.00",
"period_start": "2026-06-16",
"period_end": "2026-06-30",
"processed_at": "2026-06-30T18:02:00Z",
"paid_at": "2026-06-30T18:02:20Z",
"created_at": "2026-06-30T18:00:00Z"
}
],
"has_more": true,
"next_cursor": "c2Vfb2Zmc2V0OjEwOA"
}
Notable errors: 403 insufficient_scope, 400 invalid_request (unknown status filter).
GET /payouts/{id}
Fetch one payout. Scope: payouts:read.
Notable errors: 404 not_found, 403 insufficient_scope.
POST /payouts
Schedule a payout for a vendor and period. Scope: payouts:manage. An Idempotency-Key is required — a retried request must never create a second payout.
| Field | Type | Notes |
|---|---|---|
vendor_id |
string | Required. ven_… |
amount |
number | Required, > 0 |
currency |
string | Default USD (3-letter code) |
period_start |
date | Required (YYYY-MM-DD) |
period_end |
date | Required, on or after period_start |
payout_method |
string | null | Optional |
notes |
string | null | Optional, max 2000 chars |
Creates a pending payout and emits payout.scheduled.
curl https://vendoriq.kernvellabs.com/api/public/v1/payouts \
-X POST \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3a1b2c3d-4e5f-4607-8918-2a3b4c5d6e7f" \
-d '{ "vendor_id": "ven_9f3a1c22", "amount": 108.00, "period_start": "2026-06-16", "period_end": "2026-06-30" }'
Notable errors: 400 invalid_request (missing Idempotency-Key, bad vendor id, or period_end before period_start), 404 not_found (vendor), 422 unprocessable (couldn't schedule), 409 idempotency_conflict, 403 insufficient_scope, 403 feature_not_in_plan.
POST /payouts/{id}/complete
Mark a payout completed (settled) — for when your own accounting system confirms the money landed. Scope: payouts:manage. Send an Idempotency-Key.
| Field | Type | Notes |
|---|---|---|
external_reference |
string | null | Required for a live completion: the external payment reference (max 255 chars). A test-key preview may omit it. |
Records status: "completed" (with processed_at + paid_at) and produces payout.completed. This endpoint does not transfer funds. An already-completed payout returns its recorded state without another completion event; a cancelled or failed payout returns 409.
curl https://vendoriq.kernvellabs.com/api/public/v1/payouts/pyt_2c9d/complete \
-X POST \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d4c3b2a-1f09-4827-9e6f-3829401770a1" \
-d '{ "external_reference": "PAYOUTBATCH-8842" }'
Notable errors: 400 invalid_request (missing live payment reference), 404 not_found, 409 conflict (payout is cancelled or failed), 409 idempotency_conflict, 403 insufficient_scope.
JavaScript example — schedule a payout
const res = await fetch("https://vendoriq.kernvellabs.com/api/public/v1/payouts", {
method: "POST",
headers: {
Authorization: "Bearer kv_ven_live_xxxxxxxx",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(), // required on POST /payouts
},
body: JSON.stringify({
vendor_id: "ven_9f3a1c22",
amount: 108.0,
period_start: "2026-06-16",
period_end: "2026-06-30",
}),
});
const payout = await res.json();
console.log(payout.id, payout.status); // "pyt_…", "pending"
Commissions
Read your store's commission rules. Rule authoring stays in the VendorIQ admin (global / by category / by vendor / by product / by order-size tier); this resource is read-only.
Commission rule object fields
| Field | Type | Notes |
|---|---|---|
id |
string | cmr_… |
object |
string | "commission_rule" |
name |
string | Rule name |
type |
string | global, vendor, category, product, tiered |
rate |
string | null | The rate (percentage or fixed amount) |
rate_type |
string | percentage, fixed_per_order, fixed_per_item, margin |
priority |
integer | Higher wins on a tie |
is_active |
boolean | Whether the rule is active |
scope_vendor_id |
string | null | ven_… for vendor-scoped rules |
scope_product_type |
string | null | Product type for category-scoped rules |
effective_from / effective_until |
string | null | Time window (UTC) |
created_at |
string | UTC |
GET /commissions
List commission rules, newest first. Scope: commissions:read.
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25 |
cursor |
query | string | Pagination cursor |
curl "https://vendoriq.kernvellabs.com/api/public/v1/commissions?limit=2" \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
{
"object": "list",
"data": [
{
"id": "cmr_5a1b",
"object": "commission_rule",
"name": "Default 15%",
"type": "global",
"rate": "15.00",
"rate_type": "percentage",
"priority": 0,
"is_active": true,
"scope_vendor_id": null,
"scope_product_type": null,
"effective_from": null,
"effective_until": null,
"created_at": "2026-05-01T08:00:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Notable errors: 403 insufficient_scope, 400 invalid_request.
GET /commissions/{id}
Fetch one commission rule. Scope: commissions:read.
Notable errors: 404 not_found, 403 insufficient_scope.
Vendor Products
The products assigned to your vendors. Read them, and update a product's approval status.
Vendor product object fields
| Field | Type | Notes |
|---|---|---|
id |
string | vpr_… |
object |
string | "vendor_product" |
vendor_id |
string | ven_… |
title |
string | Product title |
shopify_product_id |
integer | The Shopify product id |
product_type |
string | null | Shopify product type |
status |
string | pending, approved, rejected, active, archived |
publication_status |
string | Last publication state recorded by VendorIQ, or unconfirmed when no result is recorded; this is not a fresh Shopify storefront check |
commission_rate / commission_type |
string | null | Per-product commission override |
price_min / price_max |
string | null | Price range |
inventory_total |
integer | null | Total inventory |
created_at |
string | UTC |
GET /vendor_products
List vendor products, newest first. Scope: vendor_products:read.
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25 |
cursor |
query | string | Pagination cursor |
status |
query | string | Filter: pending, approved, rejected, active, archived |
vendor_id |
query | string | Filter by vendor (ven_…) |
Notable errors: 403 insufficient_scope, 400 invalid_request.
GET /vendor_products/{id}
Fetch one vendor product. Scope: vendor_products:read.
Notable errors: 404 not_found, 403 insufficient_scope.
PATCH /vendor_products/{id}
Update a vendor product's approval status. Scope: vendor_products:write. Send an Idempotency-Key.
| Field | Type | Notes |
|---|---|---|
status |
string | Change to approved, rejected, or archived; pending and active may appear when reading an existing product but are not transition targets |
reason |
string | Optional only with rejected, up to 2,000 characters |
This action uses the same approval workflow as the merchant admin. Rejection or archiving confirms Shopify takedown before changing the local approval state. Approval records the merchant's decision and attempts to publish to Shopify; a response with status: "approved" does not by itself confirm that the listing is live on the storefront. Read publication_status for VendorIQ's last recorded publication outcome, and verify the live Shopify listing before acting on current storefront visibility. Updates emit vendor_product.updated.
curl https://vendoriq.kernvellabs.com/api/public/v1/vendor_products/vpr_4d2e \
-X PATCH \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2b3c4d5e-6f70-4811-9a2b-3c4d5e6f7081" \
-d '{ "status": "approved" }'
Notable errors: 404 not_found, 400 invalid_request (unsupported transition or misplaced reason), 502 service_unavailable (Shopify takedown unconfirmed; local approval is unchanged), 409 idempotency_conflict, 403 insufficient_scope.
Invoices
Per-vendor invoices generated by VendorIQ's payout/invoice flow — a period summary of a vendor's gross revenue, commission, and net payout. Read-only.
Invoice object fields
| Field | Type | Notes |
|---|---|---|
id |
string | inv_… |
object |
string | "invoice" |
vendor_id |
string | ven_… |
invoice_number |
string | Human-readable invoice id |
status |
string | Invoice status |
currency |
string | e.g. USD |
gross_revenue |
string | null | Gross for the period |
commission_total |
string | null | Commission for the period |
payout_total |
string | null | Net paid for the period |
order_count |
integer | Orders covered |
period_start / period_end |
string | null | Period (UTC) |
issued_at |
string | null | When issued (UTC) |
created_at |
string | UTC |
GET /invoices
List invoices, newest first. Scope: invoices:read.
| Param | In | Type | Description |
|---|---|---|---|
limit |
query | integer | 1–100, default 25 |
cursor |
query | string | Pagination cursor |
vendor_id |
query | string | Filter by vendor (ven_…) |
Notable errors: 403 insufficient_scope, 400 invalid_request.
GET /invoices/{id}
Fetch one invoice. Scope: invoices:read.
Notable errors: 404 not_found, 403 insufficient_scope.
Webhooks
Subscribe to VendorIQ events for asynchronous updates about vendors, orders, products, and payouts. Reconcile important resource state with the matching GET endpoint; webhook delivery is not a real-time or funds-movement authority. See Webhook events for the catalog and delivery/signature details.
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 |
include_pii |
boolean | Whether buyer PII may ride on this subscription's payloads |
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 (whsec_…) exactly once.
New subscriptions return 201. Exact retries with the same URL, environment, and event set return 200 with the existing subscription and an Idempotent-Replay: true header — the secret is not re-returned (reuse the one you stored). Whether a subscription can carry buyer PII is set from the creating key's own customers:read entitlement (and is re-checked against your plan at delivery time).
| 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://vendoriq.kernvellabs.com/api/public/v1/webhooks \
-X POST \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/hooks/vendoriq",
"events": ["payout.completed", "vendor_order.fulfilled"],
"description": "Accounting sync" }'
{
"id": "whk_77c1",
"object": "webhook_subscription",
"url": "https://yourapp.com/hooks/vendoriq",
"events": ["payout.completed", "vendor_order.fulfilled"],
"description": "Accounting sync",
"env": "live",
"is_active": true,
"include_pii": false,
"created_at": "2026-07-01T10: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, or an unknown event type), 403 insufficient_scope.
GET /webhooks
List your subscriptions (the signing secret is never returned again). Scope: webhooks:manage.
curl https://vendoriq.kernvellabs.com/api/public/v1/webhooks \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
GET /webhooks/{id}
Fetch one subscription (no secret). Scope: webhooks:manage.
Notable errors: 404 not_found, 403 insufficient_scope.
DELETE /webhooks/{id}
Delete a subscription. Scope: webhooks:manage.
curl https://vendoriq.kernvellabs.com/api/public/v1/webhooks/whk_77c1 \
-X DELETE \
-H "Authorization: Bearer kv_ven_live_xxxxxxxx"
{ "object": "webhook_subscription", "id": "whk_77c1", "deleted": true }
Notable errors: 404 not_found, 403 insufficient_scope.
Webhook events
VendorIQ supports these 9 event types. Subscribe to any of them via POST /webhooks. Test-mode subscriptions receive only test-mode events; live subscriptions only live events.
| Event | Fires when | data payload |
|---|---|---|
vendor.created |
A vendor is created | A vendor object |
vendor.updated |
A vendor is updated | A vendor object |
vendor.onboarded |
A vendor completes payout onboarding | A vendor-onboarding summary (business identity + payout readiness) |
vendor_order.created |
A per-vendor order split is created | A vendor order object |
vendor_order.fulfilled |
An order split is marked fulfilled | A vendor order object |
vendor_product.updated |
A captured change to a vendor product's public fields | A vendor product object |
vendor_product.deleted |
VendorIQ records a current vendor product deletion | A tombstone with object: "vendor_product_tombstone", public product id, public vendor_id, and deleted_at |
payout.scheduled |
A payout is scheduled | A payout object |
payout.completed |
A payout is marked completed | A payout object |
Create, update, fulfillment, and payout event
datauses the matching GET resource fields.vendor.onboardeduses an onboarding summary;vendor_product.deleteduses the tombstone above and does not confirm deletion on Shopify. Buyer PII onvendor_order.*payloads is only included when the subscription is entitled to it (customers:readat creation) and your current plan still allows it — a two-layer gate re-checked at delivery.
Delivery envelope (what we POST to your URL):
{
"id": "evt_9a2f1c",
"type": "payout.completed",
"api_version": "2026-06-23",
"created_at": "2026-07-01T10:05:25Z",
"data": {
"id": "pyt_2c9d",
"object": "payout",
"vendor_id": "ven_9f3a1c22",
"payout_number": "NSC-2026-06-30-9F3A1C22",
"amount": "108.00",
"currency": "USD",
"status": "completed",
"payout_method": "paypal",
"order_count": 1,
"period_start": "2026-06-16",
"period_end": "2026-06-30",
"paid_at": "2026-07-01T10:05:20Z",
"created_at": "2026-06-30T18:00:00Z"
}
}
Verify every delivery. Each POST carries the header:
X-Kernvel-Signature: t=<unix-timestamp>,v1=<hmac-hex>
Compute v1 = HMAC_SHA256(signing_secret, "<t>.<raw-request-body>") and compare in constant time. Reject any delivery whose timestamp t is more than 5 minutes old (replay protection). Use the whsec_… secret you stored at subscription creation.
import crypto from "node:crypto";
// Express-style handler. Mount with express.raw({ type: "application/json" })
// before any JSON parser so req.body is the exact request Buffer.
export function handleVendorIQWebhook(req, res) {
const secret = process.env.VENDORIQ_WEBHOOK_SECRET; // the whsec_ shown once
const header = req.headers["x-kernvel-signature"]; // "t=...,v1=..."
if (typeof header !== "string" || !Buffer.isBuffer(req.body))
return res.status(400).send("bad signature");
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
if (!/^\d+$/.test(parts.t || "") || !/^[a-f0-9]{64}$/i.test(parts.v1 || ""))
return res.status(400).send("bad signature");
const signed = Buffer.concat([Buffer.from(`${parts.t}.`), req.body]);
const expected = crypto.createHmac("sha256", secret).update(signed).digest();
const provided = Buffer.from(parts.v1, "hex");
const ok = crypto.timingSafeEqual(expected, provided);
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.body.toString("utf8"));
console.log(event.type, event.data.id); // e.g. "payout.completed", "pyt_…"
res.sendStatus(200);
}
Delivery guarantees
- Delivery attempts may repeat. Return a
2xxquickly and make your handler idempotent on the eventid(evt_…). Reconcile critical payout state with GET, particularly after a provider dispatch; a webhook should not be your only record of money state. - Retries with back-off. A retryable failed delivery (non-
2xx, timeout, or connection error) may be attempted up to 5 more times. A periodic recovery worker applies increasing delays, so retry times are approximate. - Auto-disable. After 15 consecutive retry-exhausted delivery failures, VendorIQ disables the subscription. Check its status and delivery history in the admin, then reactivate it once your endpoint is healthy.
- SSRF-guarded. Each delivery re-checks that the target resolves to a public address and does not follow redirects — so a webhook can never be pointed at a private/internal host.