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
- In your Shopify admin, open GateIQ → Settings → API Keys.
- Click Create key, name it (e.g. "CRM sync"), pick the scopes (permissions) it needs, and optionally an expiry.
- Copy the key immediately — it's shown only once. Store it somewhere safe (a password manager or your server's secrets). If you lose it, revoke it and make a new one.
Keys look like kv_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 thenext_cursorfrom 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 anyPOST/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.
- Create a subscription (scope
webhooks:manage):
The response includes a signing secret (shown once).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"] }' - 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" } } - Verify every webhook using the
X-Kernvel-Signature: t=<unix>,v1=<hmac>header — computeHMAC-SHA256(secret, "<t>.<raw-body>")and compare; reject if the timestamp is older than 5 minutes.
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_emailandcustomer_nameare returned only if your key holds thecustomers:readscope and the current plan still grants it. Without that effective scope, those two fields arenull; 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_emailandcustomer_nameare delivered asnull. Usemembership_idand the memberidfor 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 || "")
);
}