← Back to ReturnsIQ overview
R

Kernvel ReturnsIQ API Reference

Read and manage your store's returns programmatically — for developers building custom integrations.

ReturnsIQ API Reference

The ReturnsIQ API lets you (or your developer) read and manage your store's returns data programmatically — build custom dashboards, sync returns into your ERP or helpdesk, automate approvals, and pull fraud signals into your own risk tooling. Everything you can do in the app, you can increasingly do by API.

New to APIs? You don't have to use this — the app does everything from its dashboard. The API is for merchants who want to build custom integrations. If you have a developer, hand them this page; it's all they need.

Base URL

https://returnsiq.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 ReturnsIQ → Settings → API Keys.
  2. Click Create key, name it (e.g. "Helpdesk 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_ret_live_xxxxxxxx (live) or kv_ret_test_xxxxxxxx (test mode).

Authentication

Send your key as a Bearer token on every request:

curl https://returnsiq.kernvellabs.com/api/public/v1/returns \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

(X-API-Key: kv_ret_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 returns realistic previews for return creation, approve/deny actions, refunds, and policy creation without changing real returns, policies, refunds, Shopify orders, carrier labels, or customer emails. 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. returns:read, returns: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 (name, email, and Shopify customer ID) sits behind its own customers:read scope and is only available while the store's current plan grants that scope.

The scopes available on ReturnsIQ:

Scope Grants
returns:read Read returns and their line items
returns:write Create returns and approve/deny them
returns:manage Cancel returns and sensitive return operations
refunds:manage Read refunds and issue refunds
return_reasons:read Read the store's return-reason catalog
policies:read Read return policies
policies:write Create and update return policies
fraud_signals:read Read fraud scores/signals on returns
customers:read PII — read customer name, email, and Shopify customer ID on a return
webhooks:manage Create and manage webhook subscriptions
account:read Read your plan, usage, and API limits

Plans

API access is part of your ReturnsIQ plan. In general: Free = test keys only; Starter = read access; Pro/Scale/Enterprise = full read/write/manage + webhooks + customer data. 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://returnsiq.kernvellabs.com/api/public/v1/returns?limit=50&cursor=c2Vfb..." \
      -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
    
  • Filtering/sorting: each list endpoint documents its filters (e.g. ?status=requested&sort=requested_at&order=desc).
  • 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 refund never happens twice. Required on money/state actions (approve, deny, issue refund, create return).

Responses

  • A single item is returned directly; lists are wrapped:
    { "object": "list",
      "data": [ { "id": "ret_8fk3", "object": "return", "...": "..." } ],
      "has_more": true, "next_cursor": "c2Vfb..." }
    
  • Money is a string with a currency ("refund_amount": "120.00", "currency": "USD"). Times are UTC ISO-8601 (2026-06-24T10:00:00Z). IDs are stable strings with a type prefix (ret_, rti_, rfd_, rsn_, pol_, frd_).
  • 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). Write and heavy endpoints (refunds, fraud) have their own lower limits, noted on each.

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 'returns:write'.",
  "errors": [ { "field": "scope", "message": "Required: returns: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 — a return is requested, approved, denied, received, or refunded, or a return crosses into the high fraud-risk band.

  1. Create a subscription (scope webhooks:manage):
    curl https://returnsiq.kernvellabs.com/api/public/v1/webhooks -X POST \
      -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://yourapp.com/hooks/returnsiq", "events": ["return.requested"] }'
    
    The response includes a signing secret (shown once).
  2. We POST an event envelope:
    { "id": "evt_8fk3", "type": "return.requested", "api_version": "2026-06-24",
      "created_at": "2026-06-24T10: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, record delivery attempts, and show failure counts on the subscription; 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://returnsiq.kernvellabs.com/api/public/v1/openapi.json (import into your tooling).
  • Postman collection: download the ReturnsIQ 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

The ReturnsIQ API exposes the objects that make up a return: the returns themselves, their return items, the refunds you issue against them, the return reasons catalog, your policies, and the AI fraud signals. Plus account (your plan and usage) and webhooks.

Every return flows through a state machine: requested → photo_required → under_review → approved → label_sent → in_transit → received → restocked → refunded, with rejected and cancelled as terminal off-ramps. The API enforces it — you can't refund a return that was never approved, and trying returns 409 conflict.

Returns

A return (object return, id prefix ret_) is the central record — one customer's request to send back one or more items from a Shopify order. It carries the RMA code, the order reference, status, money totals, the AI fraud score and decision, and the lifecycle timestamps.

GET /returns

List returns for your store, newest first. Cursor-paginated.

Scope: returns:read

Param Type Description
limit integer 1–100, default 25
cursor string next_cursor from a previous page
status string Filter by state: requested, photo_required, under_review, approved, label_sent, in_transit, received, restocked, refunded, rejected, cancelled, or the convenience value open (= requested + photo_required + under_review)

Returns are always ordered newest-first (by id descending); there are no sort/order parameters.

curl "https://returnsiq.kernvellabs.com/api/public/v1/returns?status=under_review&limit=20" \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "return",
      "id": "ret_8fk3",
      "rma_code": "RMA-7G2K9",
      "status": "under_review",
      "outcome": null,
      "customer_preferred_outcome": "refund",
      "reason": "wrong_size",
      "currency": "USD",
      "total_value": "129.00",
      "refund_amount": null,
      "restocking_fee": null,
      "fraud_risk_band": "low",
      "shopify_order_id": 5012345678901,
      "shopify_order_number": "#1042",
      "item_count": 1,
      "requested_at": "2026-06-24T09:14:00Z",
      "decided_at": null,
      "refunded_at": null,
      "created_at": "2026-06-24T09:14:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c2Vfb3Vmazlk"
}

Notable errors: 400 invalid_request (unknown status filter or malformed cursor), 403 insufficient_scope (key lacks returns:read).

GET /returns/{id}

Fetch a single return. (Read its line items with GET /returns/{id}/items.)

Scope: returns:read (add customers:read to receive the customer PII fields — customer_email, customer_name, shopify_customer_id; without an effective current-plan grant those fields are null)

curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_8fk3 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "return",
  "id": "ret_8fk3",
  "rma_code": "RMA-7G2K9",
  "status": "approved",
  "outcome": "refund",
  "customer_preferred_outcome": "refund",
  "reason": "wrong_size",
  "currency": "USD",
  "total_value": "129.00",
  "refund_amount": "116.10",
  "restocking_fee": "12.90",
  "fraud_risk_band": "low",
  "shopify_order_id": 5012345678901,
  "shopify_order_number": "#1042",
  "item_count": 1,
  "requested_at": "2026-06-24T09:14:00Z",
  "decided_at": "2026-06-24T11:02:00Z",
  "refunded_at": null,
  "created_at": "2026-06-24T09:14:00Z",
  "customer_email": "jordan@example.com",
  "customer_name": "Jordan Lee",
  "shopify_customer_id": 6789012345
}

The customer_email, customer_name, and shopify_customer_id fields appear only when the key holds customers:read and the store's current plan still grants it; otherwise they are null.

Notable errors: 404 not_found (no such return, or it belongs to another store — we return 404 rather than 403 so we never reveal another store's data).

POST /returns

Create a return on a customer's behalf — e.g. from your own portal or helpdesk. Idempotent.

Scope: returns:write · Send Idempotency-Key (required — this is a state-moving write)

Field Type Required Description
order_number string yes The Shopify order number the items came from (e.g. "1042")
email string yes Email on the order (used to match the order + notify)
items array yes Line items to return, at least one (see below)
reason string no A reason slug from GET /return_reasons
customer_preferred_outcome string no refund, store_credit, exchange, or repair (advisory)
customer_notes string no Extra context to show staff

Each items[] entry takes shopify_line_item_id (integer, required) plus optional quantity (default 1), unit_value, condition, reason, sku, product_title, variant_title, shopify_product_id, and shopify_variant_id. Unknown fields are rejected.

curl https://returnsiq.kernvellabs.com/api/public/v1/returns -X POST \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1a3c9e-2b4d-4f8a-9c1e-7a2b3c4d5e6f" \
  -d '{
        "order_number": "1042",
        "email": "jordan@example.com",
        "customer_preferred_outcome": "refund",
        "reason": "wrong_size",
        "items": [
          { "shopify_line_item_id": 13987654321, "quantity": 1, "condition": "new", "reason": "wrong_size" }
        ]
      }'

Returns the created return at 201 with status requested (the AI fraud score and decision are computed and attached within seconds; poll the return or subscribe to return.requested):

{
  "object": "return",
  "id": "ret_9aa2",
  "rma_code": "RMA-3P8Q1",
  "status": "requested",
  "outcome": null,
  "customer_preferred_outcome": "refund",
  "reason": "wrong_size",
  "currency": "USD",
  "total_value": "129.00",
  "refund_amount": null,
  "restocking_fee": null,
  "fraud_risk_band": null,
  "shopify_order_id": 5012345678901,
  "shopify_order_number": "#1042",
  "item_count": 1,
  "requested_at": "2026-06-24T12:00:00Z",
  "decided_at": null,
  "refunded_at": null,
  "created_at": "2026-06-24T12:00:00Z"
}

Notable errors: 404 not_found (no order matches that order number + email), 422 unprocessable (a line item isn't on the order, or the order is otherwise unprocessable), 409 idempotency_conflict (same Idempotency-Key reused with a different body) / 409 conflict (billing-limit reached).

POST /returns/{id}/approve

Approve a return. Idempotent. The transition must be legal for the return's current state (see the state machine above) or you get 409 conflict. Emits return.approved.

Scope: returns:write · Send Idempotency-Key

No request body is required.

curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_9aa2/approve -X POST \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
  -H "Idempotency-Key: 1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f"

Returns the updated return at 200:

{
  "object": "return",
  "id": "ret_9aa2",
  "rma_code": "RMA-3P8Q1",
  "status": "approved",
  "outcome": "refund",
  "customer_preferred_outcome": "refund",
  "reason": "wrong_size",
  "currency": "USD",
  "total_value": "129.00",
  "refund_amount": "116.10",
  "restocking_fee": "12.90",
  "fraud_risk_band": "low",
  "shopify_order_id": 5012345678901,
  "shopify_order_number": "#1042",
  "item_count": 1,
  "requested_at": "2026-06-24T12:00:00Z",
  "decided_at": "2026-06-24T12:05:00Z",
  "refunded_at": null,
  "created_at": "2026-06-24T12:00:00Z"
}

Notable errors: 409 conflict (illegal transition, e.g. approving an already-refunded return), 404 not_found.

POST /returns/{id}/deny

Deny (reject) a return. Idempotent. Emits return.denied. The transition must be legal for the return's current state or you get 409 conflict.

Scope: returns:write · Send Idempotency-Key

Field Type Required Description
reason string no Why the return was denied (recorded as a note; max 2000 chars)
curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_9aa2/deny -X POST \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a" \
  -d '{ "reason": "Outside the 30-day return window." }'

Returns the updated return at 200 with status rejected.

Notable errors: 409 conflict (illegal transition), 404 not_found.

JavaScript (fetch) — list returns awaiting review

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY; // kv_ret_live_...

const res = await fetch(`${BASE}/returns?status=under_review&limit=50`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) throw new Error(`ReturnsIQ ${res.status}: ${await res.text()}`);

const { data } = await res.json();
for (const r of data) {
  console.log(`${r.rma_code} — ${r.fraud_risk_band || "unscored"} risk — $${r.total_value}`);
}

Return Items

A return item (object return_item, id prefix rti_) is one line of a return — a specific product/variant, its quantity, value, customer-reported condition, and the per-item outcome (part of a return can be approved while another part is denied). Items are returned inline on GET /returns/{id}; this endpoint lets you read them directly.

GET /returns/{id}/items

List the line items on a return.

Scope: returns:read

curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_8fk3/items \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "return_item",
      "id": "rti_2210",
      "return_id": "ret_8fk3",
      "shopify_line_item_id": 13987654321,
      "shopify_product_id": 7654321098765,
      "shopify_variant_id": 41234567890123,
      "product_title": "Merino Crew Sweater",
      "variant_title": "Navy / M",
      "sku": "MCS-NVY-M",
      "quantity": 1,
      "unit_value": "129.00",
      "total_value": "129.00",
      "condition": "new",
      "reason": "wrong_size",
      "outcome": "refund",
      "restocking_eligible": true,
      "created_at": "2026-06-24T09:14:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 404 not_found (no such return for this store), 403 insufficient_scope.

GET /return_items/{id}

Fetch a single return line item directly by its id.

Scope: returns:read

curl https://returnsiq.kernvellabs.com/api/public/v1/return_items/rti_2210 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Returns a single return_item object (same shape as the list entries above).

Notable errors: 404 not_found (no such item, or its parent return belongs to another store), 403 insufficient_scope.

JavaScript (fetch) — total the value of a return's items

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

const res = await fetch(`${BASE}/returns/ret_8fk3/items`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
const { data } = await res.json();
const total = data.reduce((sum, it) => sum + Number(it.total_value), 0);
console.log(`Returning ${data.length} line(s), total $${total.toFixed(2)}`);

Refunds

A refund (object refund, id prefix rfd_) is the money movement against a return — a cash refund to the original payment method (its method also records store-credit, exchange, or repair outcomes recorded in the app). Issuing a refund is the most sensitive write in the API, so it lives behind its own refunds:manage scope and requires an idempotency key — a retried request never refunds twice. The same refunds:manage scope also grants read access to refunds.

A refund's status is one of succeeded, failed, or pending; decided_by records who issued it (e.g. manual, ai_auto, or public_api).

GET /refunds

List every refund for your store, newest first. Cursor-paginated.

Scope: refunds:manage

Param Type Description
limit integer 1–100, default 25
cursor string next_cursor from a previous page
return_id string Filter to one return (ret_...)
curl "https://returnsiq.kernvellabs.com/api/public/v1/refunds?limit=50" \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "refund",
      "id": "rfd_5521",
      "return_id": "ret_8fk3",
      "method": "refund",
      "amount": "116.10",
      "currency": "USD",
      "status": "succeeded",
      "decided_by": "ai_auto",
      "shopify_refund_id": 998877665544,
      "failure_reason": null,
      "processed_at": "2026-06-24T13:20:00Z",
      "created_at": "2026-06-24T13:19:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

GET /refunds/{id}

Fetch a single refund.

Scope: refunds:manage

curl https://returnsiq.kernvellabs.com/api/public/v1/refunds/rfd_5521 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Notable errors: 404 not_found (no such refund for this store).

GET /returns/{id}/refunds

List the refunds issued against a return (usually one, occasionally several for partial outcomes).

Scope: refunds:manage

curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_8fk3/refunds \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "refund",
      "id": "rfd_5521",
      "return_id": "ret_8fk3",
      "method": "refund",
      "amount": "116.10",
      "currency": "USD",
      "status": "succeeded",
      "decided_by": "ai_auto",
      "shopify_refund_id": 998877665544,
      "failure_reason": null,
      "processed_at": "2026-06-24T13:20:00Z",
      "created_at": "2026-06-24T13:19:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

POST /returns/{id}/refunds

Issue a cash refund to the original payment method against a return. Idempotent — required. Emits refund.issued.

Scope: refunds:manage · Send Idempotency-Key (required)

Field Type Required Description
amount number yes Gross refund amount in the return's currency (must be > 0)
reason string no Recorded on the refund record (max 2000 chars)
curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_8fk3/refunds -X POST \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b" \
  -d '{ "amount": 116.10 }'

Returns the refund at 201:

{
  "object": "refund",
  "id": "rfd_5530",
  "return_id": "ret_8fk3",
  "method": "refund",
  "amount": "116.10",
  "currency": "USD",
  "status": "succeeded",
  "decided_by": "public_api",
  "shopify_refund_id": 998877665544,
  "failure_reason": null,
  "processed_at": "2026-06-24T13:25:00Z",
  "created_at": "2026-06-24T13:25:00Z"
}

Idempotency is mandatory here. If your network drops the response and you retry with the same Idempotency-Key, you get the original refund back verbatim — the money moves once. Reusing the same key with a different body returns 409 idempotency_conflict. Use a fresh UUID per distinct refund.

Notable errors: 409 conflict (return not in an approved/received state, or already fully refunded), 409 idempotency_conflict, 422 unprocessable (amount exceeds the refundable balance), 403 insufficient_scope (key lacks refunds:manage).

JavaScript (fetch) — issue a refund safely with idempotency

import { randomUUID } from "node:crypto";
const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

async function issueRefund(returnId, amount) {
  const res = await fetch(`${BASE}/returns/${returnId}/refunds`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": randomUUID(), // reuse this exact key if you retry
    },
    body: JSON.stringify({ amount }),
  });
  if (!res.ok) throw new Error(`Refund failed ${res.status}: ${await res.text()}`);
  return res.json();
}

const refund = await issueRefund("ret_8fk3", 116.10);
console.log(`Issued ${refund.id} — ${refund.status}`);

Return Reasons

A return reason (object return_reason, id prefix rsn_) is an entry in your reason catalog — the dropdown a shopper picks from ("Wrong size", "Damaged", "Changed my mind"). The catalog is the union of Kernvel's built-in system reasons (available to every store) and any custom reasons you've added. Use the slug when creating returns.

GET /return_reasons

List the reasons available to your store — Kernvel's built-in system reasons plus any custom reasons you've added. Cursor-paginated.

Scope: return_reasons:read

Param Type Description
limit integer 1–100, default 25
cursor string next_cursor from a previous page
curl https://returnsiq.kernvellabs.com/api/public/v1/return_reasons \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "return_reason",
      "id": "rsn_1",
      "slug": "wrong_size",
      "label": "Wrong size",
      "description": "Item didn't fit as expected.",
      "sentiment_default": "neutral",
      "is_active": true,
      "sort_order": 10,
      "system": true,
      "created_at": "2026-05-01T08:00:00Z"
    },
    {
      "object": "return_reason",
      "id": "rsn_42",
      "slug": "changed_mind",
      "label": "Changed my mind",
      "description": null,
      "sentiment_default": "negative",
      "is_active": true,
      "sort_order": 20,
      "system": false,
      "created_at": "2026-05-12T10:30:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

system is true for Kernvel's built-in reasons (available to every store) and false for reasons you've added.

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

GET /return_reasons/{id}

Fetch a single return reason (a system reason or one of your own).

Scope: return_reasons:read

curl https://returnsiq.kernvellabs.com/api/public/v1/return_reasons/rsn_42 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Returns a single return_reason object (same shape as the list entries above).

Notable errors: 404 not_found (no such reason for this store), 403 insufficient_scope.

JavaScript (fetch) — build a reason lookup map

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

const res = await fetch(`${BASE}/return_reasons`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
const { data } = await res.json();
const labels = Object.fromEntries(data.map((r) => [r.slug, r.label]));
console.log(labels["wrong_size"]); // "Wrong size"

Policies

A policy (object policy, id prefix pol_) is a rule that decides what happens to a return automatically — a set of composable conditions (order age, item value, customer history) and an action (auto-approve, require a photo, send to manual review, auto-reject, or force a specific outcome). Policies are evaluated in priority order; the first match wins. Reading and writing policies lets you version-control your returns logic outside the app.

GET /policies

List your store's policies, newest first. Cursor-paginated. Each policy carries its own priority (lower = evaluated first; the first match wins).

Scope: policies:read

Param Type Description
limit integer 1–100, default 25
cursor string next_cursor from a previous page
curl "https://returnsiq.kernvellabs.com/api/public/v1/policies?limit=50" \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "policy",
      "id": "pol_7",
      "name": "Auto-approve low-value, recent orders",
      "description": "Frictionless returns under $100 within 30 days.",
      "priority": 10,
      "is_active": true,
      "conditions": {
        "operator": "AND",
        "conditions": [
          { "type": "order_age_days", "op": "lt", "value": 30 },
          { "type": "item_value", "op": "lt", "value": 100 }
        ]
      },
      "actions": { "primary": "auto_approve" },
      "times_matched": 412,
      "times_approved": 401,
      "times_rejected": 0,
      "times_fraud_caught": 2,
      "created_at": "2026-05-01T08:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

GET /policies/{id}

Fetch a single policy.

Scope: policies:read

curl https://returnsiq.kernvellabs.com/api/public/v1/policies/pol_7 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Returns a single policy object (same shape as the list entries above).

Notable errors: 404 not_found (no such policy for this store), 403 insufficient_scope.

POST /policies

Create a policy. Idempotent. Subject to your plan's policy cap. In test mode this returns a preview policy and does not create a real rule.

Scope: policies:write · Send Idempotency-Key

Field Type Required Description
name string yes Policy name
description string no What the policy does
priority integer no Lower = evaluated first (default 100)
is_active boolean no Default true
conditions object no Composable AND/OR/NOT tree (see below); defaults to {}
actions object no { "primary": "...", "secondary": {...} }; defaults to {}

The conditions and actions objects are stored as-is and interpreted by the app's decision engine. conditions supports nested operator (AND/OR/NOT) blocks; each leaf is { "type": "<field>", "op": "lt|lte|eq|gte|gt|in", "value": <v> }, with types such as order_age_days, item_value, customer_lifetime_returns, customer_vip. actions.primary is one of auto_approve, require_photo, manual_review, auto_reject, route_to_tier; actions.secondary can carry { "force_outcome": "store_credit", "restocking_fee_pct": 10 }.

curl https://returnsiq.kernvellabs.com/api/public/v1/policies -X POST \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e" \
  -d '{
        "name": "High-value items need a photo",
        "priority": 20,
        "conditions": {
          "operator": "AND",
          "conditions": [ { "type": "item_value", "op": "gte", "value": 250 } ]
        },
        "actions": { "primary": "require_photo" }
      }'

Returns the created policy at 201.

Notable errors: 422 validation_failed (missing/blank name), 403 feature_not_in_plan (your plan's policy cap is reached), 409 idempotency_conflict.

JavaScript (fetch) — list policies in evaluation order

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

const res = await fetch(`${BASE}/policies`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
const { data } = await res.json();
for (const p of data.sort((a, b) => a.priority - b.priority)) {
  console.log(`#${p.priority} ${p.name} → ${p.actions.primary}`);
}

Fraud Signals

A fraud signal (object fraud_signal, id prefix frd_) is the public, coarse risk assessment for a return. It exposes the merchant-actionable low/medium/high band plus a broad score bucket, not the exact score, model weights, confidence, thresholds, or cross-store network details. Every return is scored on submission; re-scoring (on photo upload or a merchant recompute) appends a new record, so this endpoint gives you a safe audit trail for downstream fraud tooling without exposing the internals someone could game.

GET /fraud_signals

List every fraud signal for your store, newest first. Cursor-paginated.

Scope: fraud_signals:read

Param Type Description
limit integer 1–100, default 25
cursor string next_cursor from a previous page
return_id string Filter to one return (ret_...)
curl "https://returnsiq.kernvellabs.com/api/public/v1/fraud_signals?limit=50" \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Returns a list of fraud_signal objects (same shape as below).

GET /returns/{id}/fraud_signals

List the fraud risk bands computed for a return, newest first.

Scope: fraud_signals:read

curl https://returnsiq.kernvellabs.com/api/public/v1/returns/ret_8fk3/fraud_signals \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "list",
  "data": [
    {
      "object": "fraud_signal",
      "id": "frd_3340",
      "return_id": "ret_8fk3",
      "risk_band": "low",
      "score_band": "0-30",
      "trigger": "initial_submit",
      "created_at": "2026-06-24T09:14:05Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Notable errors: 404 not_found (no such return for this store), 403 insufficient_scope (key lacks fraud_signals:read).

GET /fraud_signals/{id}

Fetch a single fraud signal by its id.

Scope: fraud_signals:read

curl https://returnsiq.kernvellabs.com/api/public/v1/fraud_signals/frd_3340 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Returns a single fraud_signal object (same shape as the list entries above).

Notable errors: 404 not_found (no such signal for this store), 403 insufficient_scope.

JavaScript (fetch) — surface the latest fraud breakdown

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

const res = await fetch(`${BASE}/returns/ret_8fk3/fraud_signals`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
const { data } = await res.json();
const latest = data[0]; // newest first
console.log(`Risk ${latest.score}/100 (${latest.risk_band}) — confidence ${latest.confidence}`);
console.table(latest.breakdown);

Account

The account object (account, id prefix shop_) is your store's plan, status, and API capabilities. Use it to discover what your current plan allows before attempting a write, and to read your live rate-limit snapshot.

GET /account

Read your plan, subscription status, currency, and API access block.

Scope: account:read

curl https://returnsiq.kernvellabs.com/api/public/v1/account \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "account",
  "id": "shop_184",
  "shop_domain": "northwind-apparel.myshopify.com",
  "plan": "scale",
  "subscription_status": "active",
  "currency": "USD",
  "test_mode": false,
  "api_access": {
    "live_keys": true,
    "max_keys": 10,
    "scopes": ["returns:read", "returns:write", "returns:manage", "refunds:manage",
               "policies:read", "policies:write", "return_reasons:read",
               "fraud_signals:read", "customers:read", "webhooks:manage", "account:read"],
    "rate_per_min": 300,
    "webhook_endpoints": 10
  }
}

GET /account/usage

Read your current usage and live rate-limit counters.

Scope: account:read

curl https://returnsiq.kernvellabs.com/api/public/v1/account/usage \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{
  "object": "usage",
  "plan": "scale",
  "returns_this_month": 1284,
  "returns_limit": 5000,
  "rate_limit": 300,
  "rate_limit_remaining": 297
}

(returns_limit is -1 on unlimited plans.)

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

JavaScript (fetch) — check a scope is available before writing

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

const res = await fetch(`${BASE}/account`, { headers: { Authorization: `Bearer ${KEY}` } });
const account = await res.json();
if (!account.api_access.scopes.includes("refunds:manage")) {
  throw new Error(`Plan "${account.plan}" can't issue refunds via API — upgrade required.`);
}

Webhooks

Manage the endpoints we POST events to. Create a subscription to receive the webhook events below; the signing secret is returned once at creation — store it to verify deliveries.

POST /webhooks

Create a webhook subscription. The destination URL is validated (must be https and a public host).

New subscriptions return 201. Exact retries with the same URL, live/test environment, and event set return 200 with the existing subscription and an Idempotent-Replay: true response header. The signing secret is NOT re-returned on a replay (it is shown only once, on the original 201 create) — reuse the secret you stored then.

Scope: webhooks:manage

Field Type Required Description
url string yes Your HTTPS endpoint
events array yes One or more event types (see Webhook events)
description string no A label for the subscription
curl https://returnsiq.kernvellabs.com/api/public/v1/webhooks -X POST \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://yourapp.com/hooks/returnsiq",
        "events": ["return.requested", "return.approved", "return.denied", "refund.issued"],
        "description": "Sync returns into our OMS"
      }'
{
  "object": "webhook_subscription",
  "id": "whk_61",
  "url": "https://yourapp.com/hooks/returnsiq",
  "events": ["return.requested", "return.approved", "return.denied", "refund.issued"],
  "description": "Sync returns into our OMS",
  "env": "live",
  "is_active": true,
  "signing_secret": "whsec_3f9aZ8...shown_once",
  "created_at": "2026-06-24T12:30:00Z"
}

Notable errors: 400 invalid_request (non-HTTPS or private/loopback 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 (the signing secret is never returned again).

Scope: webhooks:manage

curl https://returnsiq.kernvellabs.com/api/public/v1/webhooks \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

GET /webhooks/{id}

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

Scope: webhooks:manage

curl https://returnsiq.kernvellabs.com/api/public/v1/webhooks/whk_61 \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"

Returns a single webhook_subscription object (without signing_secret).

Notable errors: 404 not_found (no such subscription for this store).

DELETE /webhooks/{id}

Delete a subscription. Deliveries stop immediately.

Scope: webhooks:manage

curl https://returnsiq.kernvellabs.com/api/public/v1/webhooks/whk_61 -X DELETE \
  -H "Authorization: Bearer kv_ret_live_xxxxxxxx"
{ "object": "webhook_subscription", "id": "whk_61", "deleted": true }

Notable errors: 404 not_found (no such subscription for this store).

JavaScript (fetch) — subscribe to the return lifecycle

const BASE = "https://returnsiq.kernvellabs.com/api/public/v1";
const KEY = process.env.RETURNSIQ_API_KEY;

const res = await fetch(`${BASE}/webhooks`, {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://yourapp.com/hooks/returnsiq",
    events: ["return.requested", "return.approved", "return.denied", "refund.issued"],
  }),
});
const sub = await res.json();
console.log("Store this secret now — it is shown once:", sub.signing_secret);

Webhook events

Subscribe to any of these on a webhook subscription. Every delivery is the standard envelope — id, type, api_version, created_at, and a data block carrying the resource in the same shape the REST API returns.

Event Fires when
return.requested A customer submits a return
return.approved A return is approved (by AI, policy, or staff)
return.denied A return is rejected
return.received Returned items are marked received at your warehouse
refund.issued A refund, store credit, exchange, or repair is processed
fraud.flagged A return's fraud risk crosses into the high band — band only, never the exact score

Sample delivery (return.approved):

{
  "id": "evt_b7c2",
  "type": "return.approved",
  "api_version": "2026-06-24",
  "created_at": "2026-06-24T14:02:00Z",
  "data": {
    "object": "return",
    "id": "ret_5d10",
    "rma_code": "RMA-9X4L2",
    "status": "under_review",
    "reason": "not_as_described",
    "currency": "USD",
    "total_value": "640.00",
    "fraud_risk_band": "high",
    "shopify_order_id": 5012399988776,
    "shopify_order_number": "#1098",
    "item_count": 2,
    "requested_at": "2026-06-24T13:58:00Z"
  }
}

Sample delivery (fraud.flagged) — band only: the data block is the same coarse fraud_signal object the REST API returns, carrying just the risk_band and a broad score_band bucket. The exact 0-100 score, per-signal weights, confidence, thresholds, and cross-store network details are never included (they'd let an abuser reverse-engineer the model), so this is safe to route straight into your own risk tooling:

{
  "id": "evt_f2a9",
  "type": "fraud.flagged",
  "api_version": "2026-06-24",
  "created_at": "2026-06-24T14:05:00Z",
  "data": {
    "object": "fraud_signal",
    "id": "frd_3340",
    "return_id": "ret_5d10",
    "risk_band": "high",
    "score_band": "71-100",
    "trigger": "photo_upload",
    "created_at": "2026-06-24T14:05:00Z"
  }
}

fraud.flagged fires once when a return crosses into the high band — on submission or on a later re-score (e.g. a photo upload that exposes a fake), not on every re-score that was already high. return.received fires when a return reaches the received state, whether you mark it received in the app, set its status via the API, or a carrier delivery webhook auto-advances it.

Always verify the X-Kernvel-Signature header before trusting a delivery: compute HMAC-SHA256(signing_secret, "<t>.<raw-body>"), compare it constant-time against the v1= value, and reject any delivery whose t= timestamp is older than 5 minutes. Test-mode subscriptions receive only test-mode events.