← Back to VendorIQ overview
V

Kernvel VendorIQ API Reference

Read and manage your marketplace programmatically. Endpoints, scopes, and copy-paste examples for developers.

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 the X-Kernvel-Api-Version header. 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

  1. In your Shopify admin, open VendorIQ → Settings → API Keys. Keys are minted here in the embedded admin — not through the public API itself.
  2. 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.
  3. 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 GET requests 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 separate customers:read entitlement.
  • 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:read is the scope for buyer contact details on vendor orders. These fields (customer_name, customer_email) come back null unless your key holds customers:read and 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_plan with 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 limit items (1–100, default 25). When has_more is true, pass the returned next_cursor back 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, including PATCH and 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 returns 409 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 trailing Z (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) and X-Kernvel-Api-Version. Requests that reach the per-key rate limiter also carry the X-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 a pending payout 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_limit is your plan's approved-vendor cap; -1 means unlimited (Enterprise). active_vendors counts 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 pending payout); 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 data uses the matching GET resource fields. vendor.onboarded uses an onboarding summary; vendor_product.deleted uses the tombstone above and does not confirm deletion on Shopify. Buyer PII on vendor_order.* payloads is only included when the subscription is entitled to it (customers:read at 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 2xx quickly and make your handler idempotent on the event id (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.