← Back to GateIQ overview
G

Kernvel GateIQ API Reference

Read and manage your locks, members, and memberships programmatically — for developers building custom integrations.

GateIQ API Reference

The GateIQ API lets you (or your developer) read and manage your store's GateIQ data programmatically — build custom dashboards, sync members with your CRM or email tool, automate enrollments, or report on locked-content engagement. Everything you 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://gateiq.kernvellabs.com/api/public/v1

All endpoints are under /api/public/v1. All traffic is HTTPS only.

Getting an API key

  1. In your Shopify admin, open GateIQ → Settings → API Keys.
  2. Click Create key, name it (e.g. "CRM sync"), pick the scopes (permissions) it needs, and optionally an expiry.
  3. Copy the key immediately — it's shown only once. Store it somewhere safe (a password manager or your server's secrets). If you lose it, revoke it and make a new one.

Keys look like kv_gat_live_xxxxxxxx (live) or kv_gat_test_xxxxxxxx (test mode).

Authentication

Send your key as a Bearer token on every request:

curl https://gateiq.kernvellabs.com/api/public/v1/locks \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"

(X-API-Key: kv_gat_live_xxxxxxxx works too.) Your key is tied to your store — you never pass a shop name. Unauthenticated requests get 401.

Test mode

Use a *_test_* key to build and experiment safely: test mode never sends real emails, never charges, never enrolls a real member or mints a real Shopify selling plan. Test keys are available on every plan, so you can build before you upgrade. Test responses include the header X-Kernvel-Test-Mode: true.

Permissions (scopes)

Each key only does what you allow. Scopes are resource:action, e.g. locks:read, members:write. A request missing the needed scope returns 403 insufficient_scope telling you which scope to add. Grant the least your integration needs. Customer personal data (names, emails) sits behind its own customers:read scope and is only available on higher plans.

GateIQ's scopes:

Scope What it allows
locks:read List and read locks and their conditions
locks:write Create and update locks
locks:manage Delete locks
members:read List and read members
members:write Enroll members and approve pending members
members:manage Set/migrate a member's tier
memberships:read List and read membership tiers
approval_requests:read List and read membership approval requests
analytics:read Read access-control analytics and event counts
customers:read PII — read member names and emails (top plans only)
webhooks:manage Create, list, and delete webhook subscriptions
account:read Read your plan, usage, and API limits

Plans

API access is part of your GateIQ plan. In general: Free = test keys only (read scopes, for sandboxing); Starter = read access on live keys; Pro / Scale = full read/write/manage + webhooks + PII. PII (customers:read) is Pro/Scale only. If an endpoint isn't in your plan you'll get 403 feature_not_in_plan with an upgrade link.

Requests

  • Send JSON bodies with Content-Type: application/json.
  • Pagination: list endpoints return up to 25 items by default (max 100). Use ?limit= and the next_cursor from the response to page:
    curl "https://gateiq.kernvellabs.com/api/public/v1/members?limit=50&cursor=c2Vfb..." \
      -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
    
  • Filtering/sorting: each list endpoint documents its filters (e.g. ?status=active).
  • Idempotency (important for writes): send an Idempotency-Key: <unique-id> header on any POST/PATCH/DELETE. If a request is retried (network hiccup), we return the original result instead of doing it twice — so a retried enrollment never happens twice. Recommended on every state-moving action (enroll, approve, set-tier, create/update/delete a lock).

Responses

  • A single item is returned directly; lists are wrapped:
    { "object": "list",
      "data": [ { "id": "lck_a1b2c3d4", "object": "lock", "...": "..." } ],
      "has_more": true, "next_cursor": "c2Vfb..." }
    
  • Money is a decimal string ("price": "19.00"). Times are UTC ISO-8601 (2026-06-23T10:00:00Z). IDs are stable strings with a type prefix (lck_, mbr_, mshp_, apr_, whk_).
  • Every response includes X-Request-Id (quote it in support tickets) and rate-limit headers (below).

Rate limits

Each key has a per-minute limit that scales with your plan. Every response includes:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1750671600

If you exceed it you get 429 with a Retry-After header (seconds to wait).

Errors

Errors use a standard machine-readable format (application/problem+json):

{ "type": "https://docs.kernvellabs.com/api/errors/insufficient_scope",
  "title": "Insufficient scope", "status": 403, "code": "insufficient_scope",
  "detail": "This key is missing the required scope 'members:write'.",
  "errors": [ { "field": "scope", "message": "Required: members:write" } ],
  "request_id": "req_8fk3a1" }

Common codes: invalid_request, validation_failed, invalid_api_key, expired_api_key, insufficient_scope, feature_not_in_plan, not_found, idempotency_conflict, conflict, rate_limited, internal_error. The detail, errors, and request_id fields tell you what to fix and what to quote if you need support.

Webhooks (get notified instead of polling)

Rather than polling, subscribe to events and we'll POST them to your URL the moment they happen.

  1. Create a subscription (scope webhooks:manage):
    curl https://gateiq.kernvellabs.com/api/public/v1/webhooks -X POST \
      -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://yourapp.com/hooks/gateiq", "events": ["member.enrolled"] }'
    
    The response includes a signing secret (shown once).
  2. We POST an event envelope:
    { "id": "evt_8fk3", "type": "member.enrolled", "api_version": "2026-06-23",
      "created_at": "2026-06-23T10:00:00Z", "data": { "...": "the resource" } }
    
  3. Verify every webhook using the X-Kernvel-Signature: t=<unix>,v1=<hmac> header — compute HMAC-SHA256(secret, "<t>.<raw-body>") and compare; reject if the timestamp is older than 5 minutes.

We retry failed deliveries with backoff and show a delivery log; persistent failures auto-disable the endpoint and email you. See Webhook events for the full catalog.

Versioning

The API is versioned in the URL (/api/public/v1). We won't make breaking changes to v1 — new fields may be added, but nothing is removed without a new version and 90+ days notice. The X-Kernvel-Api-Version response header shows the dated version applied.

Tools

  • OpenAPI spec: https://gateiq.kernvellabs.com/api/public/v1/openapi.json (import into your tooling).
  • Postman collection: download the GateIQ collection, or export the latest one from the API Keys page.
  • Quickstart, errors, and webhooks: this reference includes key setup, request format, error shape, signature verification, and event catalog in one page.

Need help?

Email support@kernvellabs.com with the request_id from the response — that lets us find your exact call instantly.


Resources

GateIQ's API is organized around the things you manage in the app: the locks that gate your content, the memberships (tiers) customers join, the members themselves, the approval requests for tiers that need review, and the analytics of who's hitting your locks. Everything is scoped to your store — you never pass a shop name, and you can never see another store's data.

Resource What it is
Locks The rules that gate a product, collection, page, or URL
Members A customer's enrollment in a membership tier
Memberships A membership tier (Free, VIP, Wholesale…) and its billing
Approval requests Pending applications for tiers that require approval
Analytics Access-control summaries and analytics-event counts
Account Your plan, usage, and API limits
Webhooks Manage your event subscriptions

Locks

A lock is a rule that gates a resource — a product, collection, page, blog, variant, price, cart, the whole store, or a URL pattern — behind the conditions you set (member of a tier, logged in, a customer tag, a country, and so on). When a shopper hits a locked resource they either get access or see your access-denied message.

Lock object fields

Field Type Notes
id string lck_ + id
object string Always "lock"
name string Your label for the lock
resource_type string product · collection · page · blog · variant · price · cart · store · url_pattern
resource_id string | null The gated resource's id, or a glob path for url_pattern (e.g. /collections/wholesale/**)
status string active · inactive · scheduled
priority integer Higher wins when multiple locks match
access_denied_message string | null Shown to blocked shoppers
conditions array The key conditions on the lock; each is { "type", "operator", "is_inverted" }
schedule_start string | null When a scheduled lock turns on (UTC)
schedule_end string | null When it turns off (UTC)
created_at string UTC ISO-8601
updated_at string UTC ISO-8601

GET /locks

List your locks, newest first.

Scope: locks:read

Param In Type Notes
status query string Filter: active · inactive · scheduled
resource_type query string Filter by resource type (e.g. collection)
limit query integer 1–100, default 25
cursor query string next_cursor from a previous page
curl "https://gateiq.kernvellabs.com/api/public/v1/locks?status=active&limit=2" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "lock",
      "id": "lck_a1b2c3d4",
      "name": "Wholesale catalog",
      "resource_type": "collection",
      "resource_id": "gid://shopify/Collection/41250118",
      "status": "active",
      "priority": 10,
      "access_denied_message": "This catalog is for approved wholesale members.",
      "conditions": [
        { "type": "membership", "operator": "is", "is_inverted": false }
      ],
      "schedule_start": null,
      "schedule_end": null,
      "created_at": "2026-06-21T09:14:00Z",
      "updated_at": "2026-06-21T09:14:00Z"
    },
    {
      "object": "lock",
      "id": "lck_55e7f9a0",
      "name": "VIP early-access drops",
      "resource_type": "url_pattern",
      "resource_id": "/collections/drops/**",
      "status": "scheduled",
      "priority": 5,
      "access_denied_message": "VIP members only — opens to everyone Friday.",
      "conditions": [
        { "type": "membership", "operator": "is", "is_inverted": false }
      ],
      "schedule_start": "2026-06-25T00:00:00Z",
      "schedule_end": "2026-06-27T00:00:00Z",
      "created_at": "2026-06-20T18:02:11Z",
      "updated_at": "2026-06-20T18:02:11Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 403 insufficient_scope (key lacks locks:read), 400 invalid_request (unknown status or resource_type filter).

GET /locks/{lock_id}

Fetch a single lock.

Scope: locks:read

Param In Type Notes
lock_id path string The lock id (lck_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/locks/lck_a1b2c3d4" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "lock",
  "id": "lck_a1b2c3d4",
  "name": "Wholesale catalog",
  "resource_type": "collection",
  "resource_id": "gid://shopify/Collection/41250118",
  "status": "active",
  "priority": 10,
  "access_denied_message": "This catalog is for approved wholesale members.",
  "conditions": [
    { "type": "membership", "operator": "is", "is_inverted": false }
  ],
  "schedule_start": null,
  "schedule_end": null,
  "created_at": "2026-06-21T09:14:00Z",
  "updated_at": "2026-06-21T09:14:00Z"
}

Notable errors: 404 not_found (no such lock, or it belongs to another store).

POST /locks

Create a lock. Send an Idempotency-Key to make retries safe. Returns 201.

Scope: locks:write

Field Type Notes
name string Required (1–255 chars)
resource_type string Required — one of the resource types above
resource_id string Optional (a glob path for url_pattern; omit for store)
status string Default active
priority integer Default 0
access_denied_message string Optional
conditions array Optional — key conditions in the internal builder shape
curl "https://gateiq.kernvellabs.com/api/public/v1/locks" -X POST \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0b9c1f4e-7a2d-4f0e-9c3a-1d2e3f4a5b6c" \
  -d '{
    "name": "Members-only blog",
    "resource_type": "blog",
    "resource_id": "gid://shopify/Blog/77001",
    "access_denied_message": "Join to read member stories.",
    "priority": 1
  }'
{
  "object": "lock",
  "id": "lck_9d0a1b2c",
  "name": "Members-only blog",
  "resource_type": "blog",
  "resource_id": "gid://shopify/Blog/77001",
  "status": "active",
  "priority": 1,
  "access_denied_message": "Join to read member stories.",
  "conditions": [],
  "schedule_start": null,
  "schedule_end": null,
  "created_at": "2026-06-23T10:00:00Z",
  "updated_at": "2026-06-23T10:00:00Z"
}

Notable errors: 403 insufficient_scope, 422 validation_failed (invalid body / unknown field), 409 conflict (plan limit reached), 409 idempotency_conflict (same Idempotency-Key, different body).

PATCH /locks/{lock_id}

Update a lock — rename it, change its message, raise its priority, or activate/deactivate it.

Scope: locks:write

Param In Type Notes
lock_id path string The lock id (lck_...)

Body accepts any of: name, status, priority, access_denied_message, conditions. Send only the fields you want to change.

curl "https://gateiq.kernvellabs.com/api/public/v1/locks/lck_9d0a1b2c" -X PATCH \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3e2a1b-0c9d-4e8f-a7b6-c5d4e3f2a1b0" \
  -d '{ "status": "inactive" }'
{
  "object": "lock",
  "id": "lck_9d0a1b2c",
  "name": "Members-only blog",
  "resource_type": "blog",
  "resource_id": "gid://shopify/Blog/77001",
  "status": "inactive",
  "priority": 1,
  "access_denied_message": "Join to read member stories.",
  "conditions": [],
  "schedule_start": null,
  "schedule_end": null,
  "created_at": "2026-06-23T10:00:00Z",
  "updated_at": "2026-06-23T11:42:08Z"
}

Notable errors: 404 not_found, 422 validation_failed, 409 conflict.

DELETE /locks/{lock_id}

Delete a lock. The gated resource becomes open to everyone immediately.

Scope: locks:manage

Param In Type Notes
lock_id path string The lock id (lck_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/locks/lck_9d0a1b2c" -X DELETE \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Idempotency-Key: 2c1b0a9f-8e7d-4c6b-a5f4-e3d2c1b0a9f8"
{ "object": "lock", "id": "lck_9d0a1b2c", "deleted": true }

Notable errors: 403 insufficient_scope (key lacks locks:manage), 404 not_found.

JavaScript (fetch) — list active locks

const res = await fetch(
  "https://gateiq.kernvellabs.com/api/public/v1/locks?status=active",
  { headers: { Authorization: "Bearer kv_gat_live_xxxxxxxx" } }
);
if (!res.ok) throw new Error(`GateIQ API ${res.status}`);
const { data } = await res.json();
for (const lock of data) {
  console.log(lock.id, lock.name, "→", lock.resource_type);
}

Members

A member is a single customer's enrollment in one of your membership tiers. It tracks their status, their billing state, and the dates that matter (started, expires). This is the resource most integrations care about — sync it to your CRM, email tool, or loyalty system.

Member object fields

Field Type Notes
id string mbr_ + id
object string Always "member"
membership_id string | null The tier they're on (mshp_...)
status string active · expired · cancelled · pending_approval
billing_status string | null paid · trial · overdue · free · paused · refunded
customer_email string | null PII — only with customers:read
customer_name string | null PII — only with customers:read
started_at string | null When membership began (UTC)
expires_at string | null When access ends (UTC)
created_at string UTC ISO-8601

PII: customer_email and customer_name are returned only if your key holds the customers:read scope and the current plan still grants it. Without that effective scope, those two fields are null; everything else is returned as normal. Member webhooks apply the same rule per subscription, so one endpoint can sync member lifecycle data without receiving customer contact details.

GET /members

List members, newest first. Filter by status.

Scope: members:read (add customers:read to include name/email)

Param In Type Notes
status query string active · expired · cancelled · pending_approval
limit query integer 1–100, default 25
cursor query string next_cursor from a previous page
curl "https://gateiq.kernvellabs.com/api/public/v1/members?status=active&limit=2" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "member",
      "id": "mbr_7c1d2e3f",
      "membership_id": "mshp_vip0001",
      "status": "active",
      "billing_status": "paid",
      "customer_email": "jordan@example.com",
      "customer_name": "Jordan Lee",
      "started_at": "2026-05-01T00:00:00Z",
      "expires_at": "2026-07-01T00:00:00Z",
      "created_at": "2026-05-01T00:00:00Z"
    },
    {
      "object": "member",
      "id": "mbr_9a8b7c6d",
      "membership_id": "mshp_whl0007",
      "status": "active",
      "billing_status": "free",
      "customer_email": "buyer@acmesupply.com",
      "customer_name": "Sam Rivera",
      "started_at": "2026-04-12T00:00:00Z",
      "expires_at": null,
      "created_at": "2026-04-12T00:00:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c2Vfb3RoZXJfcGFnZQ"
}

Notable errors: 403 insufficient_scope (no members:read), 403 feature_not_in_plan (key has customers:read on a plan that doesn't include PII), 400 invalid_request (unknown status filter).

GET /members/{member_id}

Fetch a single member.

Scope: members:read

Param In Type Notes
member_id path string The member id (mbr_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/members/mbr_7c1d2e3f" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"

Returns a single member object (same shape as above). Notable errors: 404 not_found.

POST /members

Enroll a customer into a tier. This is a state-moving write — send an Idempotency-Key so a retried enrollment never creates a duplicate. If the tier requires_approval, the member is created with status: pending_approval and an approval request is opened instead of granting access immediately. Returns 201.

Scope: members:write

Field Type Notes
membership_id string Required — the tier to enroll into (mshp_...)
email string Required — the customer's email (3–255 chars)
first_name string Optional
last_name string Optional
shopify_customer_id integer Optional — the Shopify customer numeric id, if known
curl "https://gateiq.kernvellabs.com/api/public/v1/members" -X POST \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4d5e6f7a-8b9c-4d0e-9f1a-2b3c4d5e6f7a" \
  -d '{
    "membership_id": "mshp_vip0001",
    "email": "jordan@example.com",
    "first_name": "Jordan",
    "last_name": "Lee"
  }'
{
  "object": "member",
  "id": "mbr_7c1d2e3f",
  "membership_id": "mshp_vip0001",
  "status": "active",
  "billing_status": "free",
  "customer_email": "jordan@example.com",
  "customer_name": "Jordan Lee",
  "started_at": "2026-06-23T10:00:00Z",
  "expires_at": null,
  "created_at": "2026-06-23T10:00:00Z"
}

Notable errors: 404 not_found (no such membership), 409 conflict (the tier is full — max_members reached), 422 validation_failed (missing email / bad membership_id), 409 idempotency_conflict.

POST /members/{member_id}/approve

Approve a pending member — moves a pending_approval member to active and fires member.approved. No request body. Idempotent — send an Idempotency-Key.

Scope: members:write

Param In Type Notes
member_id path string The member id (mbr_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/members/mbr_7c1d2e3f/approve" -X POST \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Idempotency-Key: 7a8b9c0d-1e2f-4a3b-b24c-5d6e7f8a9b0c"

Returns the updated member object (now status: active). Notable errors: 404 not_found, 409 conflict (already actioned, or the tier is full), 409 idempotency_conflict.

POST /members/{member_id}/tier

Set or migrate a member to a different tier. The member is moved onto the target membership (the old enrollment is closed and a new one started). Idempotent — send an Idempotency-Key.

Scope: members:manage

Param In Type Notes
member_id path string The member id (mbr_...)
Field Type Notes
membership_id string Required — the target tier (mshp_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/members/mbr_7c1d2e3f/tier" -X POST \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5e6f7a8b-9c0d-4e1f-a02b-3c4d5e6f7a8b" \
  -d '{ "membership_id": "mshp_whl0007" }'

Returns the updated member object. Notable errors: 403 insufficient_scope (no members:manage), 404 not_found (member or target tier), 409 conflict (e.g. target tier full), 422 validation_failed (bad membership_id).

JavaScript (fetch) — enroll a member idempotently

const res = await fetch("https://gateiq.kernvellabs.com/api/public/v1/members", {
  method: "POST",
  headers: {
    Authorization: "Bearer kv_gat_live_xxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    membership_id: "mshp_vip0001",
    email: "jordan@example.com",
    first_name: "Jordan",
  }),
});
const member = await res.json();
console.log(member.id, member.status); // mbr_...  active | pending_approval

Memberships

A membership is a tier customers can join — your Free, VIP, or Wholesale levels — along with its billing (free, one-time, or recurring) and its perks. These are read-only over the API; create and edit tiers in the GateIQ admin, then enroll members against them by id.

Membership object fields

Field Type Notes
id string mshp_ + id
object string Always "membership"
name string Tier name
description string | null Tier description
billing_type string recurring · one_time · free
price string | null Decimal string, e.g. "19.00" (null for free tiers)
billing_interval string | null monthly · yearly · weekly (recurring only)
trial_days integer Free-trial length
status string active · paused · archived
requires_approval boolean Whether new joins need review
max_members integer | null Cap on enrolled members (null = unlimited)
created_at string UTC ISO-8601

GET /memberships

List your membership tiers, newest first.

Scope: memberships:read

Param In Type Notes
limit query integer 1–100, default 25
cursor query string next_cursor from a previous page
curl "https://gateiq.kernvellabs.com/api/public/v1/memberships" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "membership",
      "id": "mshp_vip0001",
      "name": "VIP",
      "description": "Early access, member pricing, and exclusive drops.",
      "billing_type": "recurring",
      "price": "19.00",
      "billing_interval": "monthly",
      "trial_days": 7,
      "status": "active",
      "requires_approval": false,
      "max_members": null,
      "created_at": "2026-03-02T08:00:00Z"
    },
    {
      "object": "membership",
      "id": "mshp_whl0007",
      "name": "Wholesale",
      "description": "Approved B2B buyers — wholesale catalog and pricing.",
      "billing_type": "free",
      "price": null,
      "billing_interval": null,
      "trial_days": 0,
      "status": "active",
      "requires_approval": true,
      "max_members": 500,
      "created_at": "2026-03-02T08:05:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 403 insufficient_scope.

GET /memberships/{membership_id}

Fetch a single membership tier.

Scope: memberships:read

Param In Type Notes
membership_id path string The membership id (mshp_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/memberships/mshp_vip0001" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"

Returns a single membership object (same shape as above). Notable errors: 404 not_found.

JavaScript (fetch) — map tier id to name

const res = await fetch(
  "https://gateiq.kernvellabs.com/api/public/v1/memberships",
  { headers: { Authorization: "Bearer kv_gat_live_xxxxxxxx" } }
);
const { data } = await res.json();
const tiers = Object.fromEntries(data.map((m) => [m.id, m.name]));
console.log(tiers["mshp_vip0001"]); // "VIP"

Approval requests

When a tier has requires_approval: true, a customer applying to join creates an approval request instead of becoming a member immediately. GateIQ can attach an AI risk score and recommendation to help you decide. These are read-only over the API — to action a request, approve the associated member with POST /members/{member_id}/approve (which enrolls them).

Approval request object fields

Field Type Notes
id string apr_ + id
object string Always "approval_request"
membership_id string | null The tier applied for (mshp_...)
status string pending · approved · rejected
ai_risk_score number | null 0–1 risk estimate (higher = riskier)
ai_recommendation string | null e.g. approve · review · reject
reviewed_by string | null Who actioned it
reviewed_at string | null When actioned (UTC)
created_at string UTC ISO-8601

GET /approval_requests

List approval requests, newest first.

Scope: approval_requests:read

Param In Type Notes
status query string pending · approved · rejected
limit query integer 1–100, default 25
cursor query string next_cursor from a previous page
curl "https://gateiq.kernvellabs.com/api/public/v1/approval_requests?status=pending" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "approval_request",
      "id": "apr_3b4c5d6e",
      "membership_id": "mshp_whl0007",
      "status": "pending",
      "ai_risk_score": 0.12,
      "ai_recommendation": "approve",
      "reviewed_by": null,
      "reviewed_at": null,
      "created_at": "2026-06-22T15:30:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 403 insufficient_scope, 400 invalid_request (unknown status filter).

GET /approval_requests/{approval_id}

Fetch a single approval request.

Scope: approval_requests:read

Param In Type Notes
approval_id path string The approval request id (apr_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/approval_requests/apr_3b4c5d6e" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"

Returns a single approval request object (same shape as above). Notable errors: 404 not_found.

JavaScript (fetch) — auto-approve low-risk applications

const list = await fetch(
  "https://gateiq.kernvellabs.com/api/public/v1/approval_requests?status=pending",
  { headers: { Authorization: "Bearer kv_gat_live_xxxxxxxx" } }
).then((r) => r.json());

for (const req of list.data) {
  if (req.ai_recommendation === "approve" && req.ai_risk_score < 0.2) {
    // Approve via the member the request belongs to.
    // (Look up the pending member for this tier, then call /members/{id}/approve.)
    console.log("would auto-approve", req.id);
  }
}

Analytics

The analytics resource exposes rolled-up access-control metrics — how many access attempts were granted vs. blocked, and counts of the analytics events GateIQ records. These are aggregate-only (no per-customer rows). Use them to see how often shoppers hit your locks and where you're turning them away.

GET /analytics/access-summary

Access grant/deny totals over a rolling window.

Scope: analytics:read

Param In Type Notes
days query integer Window length in days, 1–90 (default 30)
curl "https://gateiq.kernvellabs.com/api/public/v1/analytics/access-summary?days=30" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "access_summary",
  "period_days": 30,
  "total_attempts": 18432,
  "granted": 12907,
  "blocked": 5525,
  "grant_rate": 70.0,
  "unique_sessions": 9241
}

Notable errors: 403 insufficient_scope, 400 invalid_request (days outside 1–90).

GET /analytics/events

Counts of recorded analytics events over a rolling window, grouped by event type.

Scope: analytics:read

Param In Type Notes
days query integer Window length in days, 1–90 (default 30)
event_type query string Optional — restrict to a single event type
curl "https://gateiq.kernvellabs.com/api/public/v1/analytics/events?days=30" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "event_counts",
  "period_days": 30,
  "events": [
    { "event_type": "lock_viewed", "count": 18432 },
    { "event_type": "member_signup", "count": 214 }
  ]
}

Notable errors: 403 insufficient_scope, 400 invalid_request (days outside 1–90).

JavaScript (fetch) — block rate this month

const s = await fetch(
  "https://gateiq.kernvellabs.com/api/public/v1/analytics/access-summary",
  { headers: { Authorization: "Bearer kv_gat_live_xxxxxxxx" } }
).then((r) => r.json());

const blockRate = s.total_attempts
  ? ((s.blocked / s.total_attempts) * 100).toFixed(1)
  : "0.0";
console.log(`Blocked ${blockRate}% of ${s.total_attempts} attempts.`);

Account

Read your store's plan, current usage, and the API limits your plan grants. Useful for showing a usage gauge in your own dashboard or checking what scopes are available before you create a key.

Scope: account:read

GET /account

curl "https://gateiq.kernvellabs.com/api/public/v1/account" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "account",
  "id": "shop_7d2a1f3c-...",
  "shop_domain": "acme.myshopify.com",
  "plan": "pro",
  "status": "active",
  "currency": "USD",
  "test_mode": false,
  "api_access": {
    "live_keys": true,
    "max_keys": 25,
    "scopes": ["account:read", "locks:read", "members:read", "memberships:read", "approval_requests:read", "analytics:read", "webhooks:manage", "locks:write", "locks:manage", "members:write", "members:manage", "customers:read"],
    "rate_per_min": 300,
    "webhook_endpoints": 5
  }
}

GET /account/usage

curl "https://gateiq.kernvellabs.com/api/public/v1/account/usage" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "usage",
  "plan": "pro",
  "active_locks": 12,
  "active_members": 549,
  "rate_limit": 300,
  "rate_limit_remaining": 297
}

Notable errors: 403 insufficient_scope (key lacks account:read).

JavaScript (fetch) — check remaining rate budget

const u = await fetch(
  "https://gateiq.kernvellabs.com/api/public/v1/account/usage",
  { headers: { Authorization: "Bearer kv_gat_live_xxxxxxxx" } }
).then((r) => r.json());
console.log(`${u.rate_limit_remaining}/${u.rate_limit} requests left this minute`);

Webhooks

Manage your event subscriptions over the API. (You can also do this from the GateIQ admin.) The full list of events is in Webhook events.

Webhook subscription fields

Field Type Notes
id string whk_ + id
object string Always "webhook_subscription"
url string Your HTTPS endpoint
events array Subscribed event types
description string | null Your label
env string live or test (matches the key that created it)
is_active boolean Auto-disabled after sustained delivery failure
includes_pii boolean Whether member webhook payloads may include customer_email and customer_name. This is set when the subscription is created and still depends on the current plan at delivery time.
created_at string UTC ISO-8601

POST /webhooks

Create a subscription. The response includes the signing secret exactly once — store it; you'll need it to verify deliveries. 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.

If the key that creates the subscription has the effective customers:read scope, member webhook payloads can include customer_email and customer_name; otherwise those fields are delivered as null. If the store later downgrades below the PII tier, existing subscriptions stop receiving those fields automatically.

Scope: webhooks:manage

Field Type Notes
url string Required — must be HTTPS and a public host (SSRF-checked)
events array Required — one or more event types (see catalog below)
description string Optional label
curl "https://gateiq.kernvellabs.com/api/public/v1/webhooks" -X POST \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/hooks/gateiq",
    "events": ["member.enrolled", "member.approved", "member.churned"],
    "description": "Sync members to CRM"
  }'
{
  "object": "webhook_subscription",
  "id": "whk_2a3b4c5d",
  "url": "https://yourapp.com/hooks/gateiq",
  "events": ["member.enrolled", "member.approved", "member.churned"],
  "description": "Sync members to CRM",
  "env": "live",
  "is_active": true,
  "includes_pii": true,
  "created_at": "2026-06-23T10:00:00Z",
  "signing_secret": "whsec_3f9a…shown_once"
}

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

GET /webhooks

List your subscriptions (secrets are never returned).

Scope: webhooks:manage

curl "https://gateiq.kernvellabs.com/api/public/v1/webhooks" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "webhook_subscription",
      "id": "whk_2a3b4c5d",
      "url": "https://yourapp.com/hooks/gateiq",
      "events": ["member.enrolled", "member.approved", "member.churned"],
      "description": "Sync members to CRM",
      "env": "live",
      "is_active": true,
      "includes_pii": true,
      "created_at": "2026-06-23T10:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

GET /webhooks/{webhook_id}

Fetch a single subscription (the secret is never returned).

Scope: webhooks:manage

Param In Type Notes
webhook_id path string The subscription id (whk_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/webhooks/whk_2a3b4c5d" \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"

Returns a single webhook subscription object (same shape as the list items above). Notable errors: 404 not_found.

DELETE /webhooks/{webhook_id}

Delete a subscription. Deliveries stop immediately.

Scope: webhooks:manage

Param In Type Notes
webhook_id path string The subscription id (whk_...)
curl "https://gateiq.kernvellabs.com/api/public/v1/webhooks/whk_2a3b4c5d" -X DELETE \
  -H "Authorization: Bearer kv_gat_live_xxxxxxxx"
{ "object": "webhook_subscription", "id": "whk_2a3b4c5d", "deleted": true }

Notable errors: 404 not_found.

JavaScript (fetch) — subscribe to member lifecycle events

const res = await fetch("https://gateiq.kernvellabs.com/api/public/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: "Bearer kv_gat_live_xxxxxxxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/hooks/gateiq",
    events: ["member.enrolled", "member.churned"],
  }),
});
const sub = await res.json();
// Persist sub.signing_secret now — it is never shown again.
console.log(sub.id, sub.signing_secret);

Webhook events

Subscribe to these to react the moment something changes in GateIQ instead of polling. Each delivery is an event envelope whose data holds the resource that changed. Verify the X-Kernvel-Signature header on every delivery (see Webhooks in the intro).

Event Fires when data payload
member.enrolled A customer joins a tier (or is migrated to one) The member object
member.approved A pending member is approved and becomes active The member object
member.churned A member is cancelled or expires, or is migrated off a tier (access to that tier ends) The member object
lock.created A new lock is created The lock object
lock.updated A lock's settings change (or it is deleted) The lock object

Test-mode subscriptions (created with a *_test_* key) receive only test-mode events; live subscriptions receive only live events.

Sample delivery

{
  "id": "evt_8fk3a1b2",
  "type": "member.enrolled",
  "api_version": "2026-06-23",
  "created_at": "2026-06-23T10:00:00Z",
  "data": {
    "object": "member",
    "id": "mbr_7c1d2e3f",
    "membership_id": "mshp_vip0001",
    "status": "active",
    "billing_status": "trial",
    "customer_email": "jordan@example.com",
    "customer_name": "Jordan Lee",
    "started_at": "2026-06-23T10:00:00Z",
    "expires_at": null,
    "created_at": "2026-06-23T10:00:00Z"
  }
}

Note: The sample above shows a PII-enabled subscription. If a subscription was created without effective customers:read, or the store no longer has a plan that grants it, customer_email and customer_name are delivered as null. Use membership_id and the member id for lifecycle sync; fetch contact details separately only when your integration truly needs them.

Verifying a delivery (Node.js)

import crypto from "node:crypto";

function verifyGateIQWebhook(signatureHeader, rawBody, signingSecret) {
  // Header: "t=1750671600,v1=<hex hmac>"
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((kv) => kv.split("="))
  );
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // >5 min old
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1 || "")
  );
}