API reference
The REST API for your own systems, the MCP endpoint for AI assistants, and the conventions every answer follows.
Last updated 28 September 2026On this page
The shape of it
Channekt has three programmatic surfaces, and they are for different callers.
-
/api/v1- the REST API, for your own systems. It is authenticated by an API key and covered under The REST API below. -
/mcp- the Model Context Protocol server. This is the one to use from an AI assistant, and the one this reference is mostly about. It is JSON-RPC 2.0 over Streamable HTTP, authenticated by a connection token you create in the product. See AI assistants and MCP for the tool catalogue. -
/v1/*- the REST surface the Channekt web application itself uses. It is authenticated by a browser session and is documented here for the routes that manage MCP connections. It changes with the application; build on/api/v1instead.
Base address: https://api.channekt.com. Everything is
JSON, everything is over TLS, and every request may carry an
X-Request-Id header holding a UUID, which comes back on the response
and appears in our logs.
Authentication
The MCP endpoint takes a connection token, created under Settings › MCP in Channekt:
Authorization: Bearer ck_mcp_<your token>
A token belongs to one workspace and to one person, carries an explicit list of permissions, expires (ninety days by default, at most a year), and can be revoked on its own without affecting anybody's sign-in. It is shown once at creation; Channekt stores only its SHA-256.
Everything the token can do is a subset of what its owner can do. Removing that person from the workspace, or changing their role, takes effect on the connection's very next request - the membership is read every time rather than trusted from the token.
A request with no token, a token that is not ours, a revoked one or an expired one all get the same answer:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Channekt MCP"
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32002,
"message": "That credential is not valid for Channekt. Create a connection under Settings, MCP, and use the token it shows you once."
}
}
The cases are deliberately indistinguishable from outside. Which one it was is on the activity list in Channekt, where you are signed in.
The /v1/* routes take a short-lived access token issued by signing in,
in the same Authorization: Bearer header. They are the web
application's own surface; a connection token does not work on them, and an access
token does not work on /mcp.
The REST API
/api/v1 is the REST API for your own systems: a warehouse system, an
ERP, a courier bridge or a script. It reads products, orders and stock across every
connected shop in one shape; creates and changes products; cancels orders and
reports parcels as shipped; sets stock levels, which Channekt then sends to every
channel; and tells your system about new orders through webhooks. It is part of the
Business plan and above.
Base address: https://api.channekt.com/api/v1
Keys are made under Settings › API keys by an owner or an administrator, and sent with every request:
Authorization: Bearer ck_api_<your key>
A key belongs to one workspace and borrows the rights of the person who made it,
narrowed to the scopes it was given. It is shown once, stored only as its SHA-256,
expires (ninety days by default, a year at most) and can be revoked on its own. It
can also be pinned to the addresses of your own servers, in which case a request
from anywhere else is refused with 403. An API key does not work on
/mcp or /v1, and a connection token or a sign-in does not
work here.
| Scope | What it allows |
|---|---|
products:read | Products and variants: SKUs, barcodes, prices, dimensions. |
products:write | Creating products, changing them, and adding or changing variants and prices. |
orders:read | Orders with their lines, totals, customer and addresses. |
orders:write | Cancelling orders, and reporting a parcel as shipped with its carrier and tracking number. |
inventory:read | Stock levels by warehouse. |
inventory:write | Setting and adjusting stock. Every change is written to the stock ledger. |
warehouses:read | The list of warehouses with their codes and addresses. |
webhooks:manage | Registering addresses to be told when orders arrive, ship or are cancelled, and removing them. Needs an owner or administrator's key. |
The full machine-readable description is at
/api/v1/openapi.json
(OpenAPI 3.1), which you can load into Postman, Insomnia or a client generator. It
needs no key.
Endpoints
| Route | What it does |
|---|---|
GET /products |
Products, most recently changed first. search,
status (comma-separated: DRAFT, ACTIVE,
ARCHIVED), limit (up to 100), page.
|
GET /products/{id} |
One product with every variant: SKU, barcodes, price, cost, weight, dimensions. |
GET /orders |
Orders from every shop, newest first, in every status unless
status says otherwise (OPEN, ON_HOLD,
CANCELLED, CLOSED). Also fulfillment,
channelAccountId, placedFrom,
placedTo, search, limit,
offset.
|
GET /orders/{id} |
One order: lines, money, customer, shipping and billing address. |
GET /inventory |
Stock in one warehouse, by SKU. warehouseId (the default
warehouse when left out), sku, productId,
limit (up to 500), after.
|
POST /inventory/changes |
Sets (SET) or moves (ADJUST) one SKU's on-hand
figure. The new figure goes out to every connected shop.
|
POST /products |
Creates a product with at least one variant. Prices are
{ "minor": 1999, "currency": "GBP" }. Each SKU must be new.
|
PATCH /products/{id} |
Changes the title, description, status, vendor, type or tags. Only what is sent changes. |
POST /products/{id}/variants |
Adds a variant: SKU, options, price, cost, RRP, weight, barcodes. |
PATCH /variants/{id} |
Changes one variant. barcodes, when sent, replaces the whole list. |
POST /orders/{id}/cancel |
Cancels an order and gives back the stock it was holding. 409 once it has shipped. |
POST /orders/{id}/shipments |
For a warehouse system that picked and packed the order itself: ships everything
the order was promised with the carrier, service,
trackingNumber and trackingUrl given, takes it out of
stock and tells the shop it came from.
|
GET /warehouses |
Every warehouse, with its code, whether it is the default, and its address. |
GET /webhooks, POST /webhooks |
Lists the webhooks, or registers one. See Webhooks. |
PATCH /webhooks/{id}, DELETE /webhooks/{id} |
Switches a webhook on or off ({ "active": true }), or removes it. |
Examples
Every example reads the key from CHANNEKT_KEY. Pick a language once and
every example on the page follows it.
Open orders placed since yesterday
curl "https://api.channekt.com/api/v1/orders?status=OPEN&placedFrom=2026-09-27T00:00:00Z" \
-H "Authorization: Bearer $CHANNEKT_KEY"
const params = new URLSearchParams({ status: 'OPEN', placedFrom: '2026-09-27T00:00:00Z' });
const response = await fetch(`https://api.channekt.com/api/v1/orders?${params}`, {
headers: { Authorization: `Bearer ${process.env.CHANNEKT_KEY}` },
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const { data } = await response.json();
console.log(data.map((order) => order.orderNumber));
<?php
$query = http_build_query(['status' => 'OPEN', 'placedFrom' => '2026-09-27T00:00:00Z']);
$ch = curl_init("https://api.channekt.com/api/v1/orders?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHANNEKT_KEY')],
]);
$page = json_decode(curl_exec($ch), true);
foreach ($page['data'] as $order) {
echo $order['orderNumber'], PHP_EOL;
}
import os
import requests
response = requests.get(
"https://api.channekt.com/api/v1/orders",
params={"status": "OPEN", "placedFrom": "2026-09-27T00:00:00Z"},
headers={"Authorization": f"Bearer {os.environ['CHANNEKT_KEY']}"},
)
response.raise_for_status()
print([order["orderNumber"] for order in response.json()["data"]])
{
"data": [
{
"id": "018f4b1a-0000-7000-8000-00000000c001",
"orderNumber": "CK-1042",
"channelOrderNumber": "#1042",
"channel": "SHOPIFY",
"shop": "Demo Trading",
"placedAt": "2026-09-27T14:03:11.000Z",
"paymentStatus": "PAID",
"fulfillmentStatus": "UNFULFILLED",
"lifecycleStatus": "OPEN",
"total": { "minor": 3598, "currency": "GBP", "decimal": "35.98" },
"lineCount": 2,
"customerName": "Jane Example"
}
],
"total": 1,
"limit": 25,
"offset": 0,
"hasMore": false
}
Every order, page by page
# Ask for the next page while "hasMore" is true, adding 100 to the offset each time.
curl "https://api.channekt.com/api/v1/orders?status=OPEN&limit=100&offset=0" \
-H "Authorization: Bearer $CHANNEKT_KEY"
const BASE = 'https://api.channekt.com/api/v1';
const headers = { Authorization: `Bearer ${process.env.CHANNEKT_KEY}` };
async function* allOrders(query = {}) {
for (let offset = 0; ; offset += 100) {
const params = new URLSearchParams({ ...query, limit: '100', offset: String(offset) });
const response = await fetch(`${BASE}/orders?${params}`, { headers });
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const page = await response.json();
yield* page.data;
if (!page.hasMore) return;
}
}
for await (const order of allOrders({ status: 'OPEN' })) {
console.log(order.orderNumber, order.total.decimal, order.total.currency);
}
<?php
function channekt(string $path, array $query = []): array
{
$ch = curl_init('https://api.channekt.com/api/v1' . $path . '?' . http_build_query($query));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHANNEKT_KEY')],
]);
$body = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new RuntimeException($body);
}
return json_decode($body, true);
}
for ($offset = 0; ; $offset += 100) {
$page = channekt('/orders', ['status' => 'OPEN', 'limit' => 100, 'offset' => $offset]);
foreach ($page['data'] as $order) {
echo $order['orderNumber'], ' ', $order['total']['decimal'], PHP_EOL;
}
if (!$page['hasMore']) break;
}
import os
import requests
BASE = "https://api.channekt.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['CHANNEKT_KEY']}"
def all_orders(**query):
offset = 0
while True:
page = session.get(f"{BASE}/orders", params={**query, "limit": 100, "offset": offset})
page.raise_for_status()
body = page.json()
yield from body["data"]
if not body["hasMore"]:
return
offset += 100
for order in all_orders(status="OPEN"):
print(order["orderNumber"], order["total"]["decimal"], order["total"]["currency"])
One order with its address
curl https://api.channekt.com/api/v1/orders/018f4b1a-0000-7000-8000-00000000c001 \
-H "Authorization: Bearer $CHANNEKT_KEY"
const response = await fetch(
'https://api.channekt.com/api/v1/orders/018f4b1a-0000-7000-8000-00000000c001',
{ headers: { Authorization: `Bearer ${process.env.CHANNEKT_KEY}` } },
);
const order = await response.json();
console.log(order.customer.name, order.shippingAddress);
for (const line of order.lines) console.log(line.sku, line.quantity, line.unitPrice.decimal);
<?php
$ch = curl_init('https://api.channekt.com/api/v1/orders/018f4b1a-0000-7000-8000-00000000c001');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHANNEKT_KEY')],
]);
$order = json_decode(curl_exec($ch), true);
echo $order['customer']['name'], PHP_EOL;
foreach ($order['lines'] as $line) {
echo $line['sku'], ' x', $line['quantity'], ' ', $line['unitPrice']['decimal'], PHP_EOL;
}
order = session.get(f"{BASE}/orders/018f4b1a-0000-7000-8000-00000000c001").json()
print(order["customer"]["name"], order["shippingAddress"])
for line in order["lines"]:
print(line["sku"], line["quantity"], line["unitPrice"]["decimal"])
Reading stock for one SKU
curl "https://api.channekt.com/api/v1/inventory?sku=DEMO-TSHIRT-M" \
-H "Authorization: Bearer $CHANNEKT_KEY"
const response = await fetch('https://api.channekt.com/api/v1/inventory?sku=DEMO-TSHIRT-M', {
headers: { Authorization: `Bearer ${process.env.CHANNEKT_KEY}` },
});
const { data } = await response.json();
for (const row of data) console.log(row.warehouseCode, row.available);
<?php
$ch = curl_init('https://api.channekt.com/api/v1/inventory?sku=DEMO-TSHIRT-M');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHANNEKT_KEY')],
]);
foreach (json_decode(curl_exec($ch), true)['data'] as $row) {
echo $row['warehouseCode'], ': ', $row['available'], PHP_EOL;
}
stock = session.get(f"{BASE}/inventory", params={"sku": "DEMO-TSHIRT-M"}).json()
for row in stock["data"]:
print(row["warehouseCode"], row["available"])
{
"warehouse": { "id": "018f4b1a-0000-7000-8000-00000000d001", "code": "MAIN", "name": "Main warehouse" },
"data": [
{
"sku": "DEMO-TSHIRT-M",
"variantId": "018f4b1a-0000-7000-8000-00000000e001",
"productId": "018f4b1a-0000-7000-8000-00000000f001",
"productTitle": "Demo T-shirt",
"warehouseId": "018f4b1a-0000-7000-8000-00000000d001",
"warehouseCode": "MAIN",
"onHand": 12,
"reserved": 2,
"available": 10,
"incoming": 0,
"damaged": 0,
"quarantine": 0,
"updatedAt": "2026-09-28T08:14:02.000Z"
}
],
"nextCursor": null
}
Setting stock after a count, safely retried. Send an
Idempotency-Key of your own - a UUID is ideal - and a retry of the same
request answers what the first one did instead of changing the figure again:
curl -X POST https://api.channekt.com/api/v1/inventory/changes \
-H "Authorization: Bearer $CHANNEKT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f9c2b1e-5d1a-4c8e-9b0a-2f4e6d8c1a3b" \
-d '{ "sku": "DEMO-TSHIRT-M", "mode": "SET", "quantity": 40, "note": "Weekly count" }'
import { randomUUID } from 'node:crypto';
const response = await fetch('https://api.channekt.com/api/v1/inventory/changes', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CHANNEKT_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({ sku: 'DEMO-TSHIRT-M', mode: 'SET', quantity: 40, note: 'Weekly count' }),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
console.log((await response.json()).data.onHand);
<?php
$ch = curl_init('https://api.channekt.com/api/v1/inventory/changes');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('CHANNEKT_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => json_encode([
'sku' => 'DEMO-TSHIRT-M',
'mode' => 'SET',
'quantity' => 40,
'note' => 'Weekly count',
]),
]);
$result = json_decode(curl_exec($ch), true);
echo $result['data']['onHand'], PHP_EOL;
import uuid
response = session.post(
f"{BASE}/inventory/changes",
headers={"Idempotency-Key": str(uuid.uuid4())},
json={"sku": "DEMO-TSHIRT-M", "mode": "ADJUST", "quantity": -3, "note": "Damaged in store"},
)
response.raise_for_status()
print(response.json()["data"]["onHand"])
A replayed answer carries Idempotent-Replayed: true. The same key with a
different body is refused with 422, and one sent while the first is
still running with 409. Keys are kept for 24 hours. SET is
naturally safe to repeat; ADJUST is not without a key.
Webhooks
Instead of asking for new orders every few minutes, register an https address and
Channekt posts to it when something happens. Register one with
POST /webhooks and a key holding webhooks:manage, naming
the events you want:
| Event | When |
|---|---|
order.created | An order arrived from any shop. |
order.cancelled | An order was cancelled, here or at the shop. |
order.payment_status_changed | An order was paid, refunded or its payment otherwise changed. |
shipment.shipped | A parcel left, from the packing bench or through the API. |
inventory.low_stock_reached | A SKU fell to its low-stock level. |
Each delivery is a POST with a small JSON body naming what changed. Read
the current state through the API rather than trusting the body: it says
which order, not everything about it.
POST https://erp.example.com/hooks/channekt
Content-Type: application/json
Channekt-Event: order.created
Channekt-Delivery: 018f4b1a-0000-7000-8000-0000000e0001
Channekt-Signature: t=1759160000,v1=5f2b...c9
{
"id": "018f4b1a-0000-7000-8000-0000000e0001",
"type": "order.created",
"occurredAt": "2026-09-29T10:00:00.000Z",
"data": { "orderId": "018f4b1a-0000-7000-8000-00000000c001" }
}
Check the signature before acting on a delivery.
Channekt-Signature is an HMAC-SHA256 of
<t>.<raw body> under the secret you were given when the
webhook was registered. Refuse one whose t is more than five minutes
old, and use Channekt-Delivery to ignore a delivery you have already
handled.
# Registering an endpoint. The answer's "secret" is shown this once.
curl -X POST https://api.channekt.com/api/v1/webhooks \
-H "Authorization: Bearer $CHANNEKT_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://erp.example.com/hooks/channekt", "events": ["order.created", "shipment.shipped"] }'
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body exactly as it arrived, before any JSON parsing.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
}
<?php
function verify(string $rawBody, string $header, string $secret): bool
{
parse_str(str_replace(',', '&', $header), $parts);
if (abs(time() - (int) ($parts['t'] ?? 0)) > 300) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}
$ok = verify(file_get_contents('php://input'), $_SERVER['HTTP_CHANNEKT_SIGNATURE'] ?? '', getenv('CHANNEKT_WEBHOOK_SECRET'));
import hashlib
import hmac
import time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
if abs(time.time() - int(parts.get("t", 0))) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Answer with any 2xx within five seconds. A delivery that fails is
tried once more and then recorded on the webhook, where GET /webhooks
shows the last status and error. After fifty failures in a row the webhook is
switched off; PATCH /webhooks/{id} with
{ "active": true } switches it back on. Deliveries are sent within a
minute of the event, and the API is always there to catch up on anything missed.
Addresses must be https and reachable from the internet.
The MCP endpoint
POST /mcp takes one JSON-RPC 2.0 message, or an array of them, and
answers with one response object, an array of them, or 202 with no
body when every message was a notification.
GET /mcp answers 405: this server sends no
server-initiated messages, so there is no stream to open.
DELETE /mcp answers 204 and does nothing - there is no
session to end, because every request authenticates on its own.
Supported protocol versions, newest first:
2025-06-182025-03-262024-11-05
A client naming a version we do not speak is answered with our newest rather than refused, which is what the specification prescribes.
Initialising:
POST /mcp
Authorization: Bearer ck_mcp_<your token>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { "protocolVersion": "2025-06-18" }
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "channekt-mcp", "title": "Channekt", "version": "1a2b3c4" }
}
}
tools/list returns only the tools this connection was
granted. A tool it cannot use is not in the list, so a model is never offered
something it will be refused for.
Calling a tool:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_inventory_discrepancies",
"arguments": { "limit": 25 }
}
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "{ ... }" }],
"structuredContent": {
"discrepancies": [
{
"listingId": "018f4b1a-0000-7000-8000-00000000a001",
"channel": "MARKETPLACE",
"shop": "Demo Trading (Marketplace)",
"sku": "DEMO-TSHIRT-M",
"warehouseCode": "MAIN",
"expected": 12,
"reported": 4,
"difference": 8,
"lastPushedAt": "2026-09-03T18:22:11Z",
"syncState": "IN_SYNC",
"lastError": null
}
],
"nextCursor": null,
"limit": 25,
"advertisingMoreThanWeHold": 0
}
}
}
The answer appears twice on purpose: structuredContent for clients
that read it, and the same object serialised in content for clients
that do not. Every identifier in these examples is fictional.
Carrying out a change is always two calls:
{ "method": "tools/call",
"params": { "name": "propose_price_update",
"arguments": { "skus": ["DEMO-TSHIRT-M"], "changeByPercent": -10 } } }
{
"actionId": "018f4b1a-0000-7000-8000-00000000b001",
"kind": "price_update",
"preview": "Change 1 price:\n DEMO-TSHIRT-M: 19.99 -> 17.99 GBP",
"warnings": [],
"approvalRequired": true,
"approvalReason": "This workspace asks a person to agree to every change an assistant proposes.",
"status": "PROPOSED",
"expiresAt": "2026-09-04T13:00:00Z"
}
After the merchant has approved it in Channekt, the assistant calls
execute_price_update with the actionId and its own
idempotencyKey. No other argument is accepted - what
runs is the stored plan, revalidated against the state of the moment. A repeated
execute with the same key returns the first result rather than doing the work
twice.
Managing connections
These are /v1/* routes, reached with a signed-in session, and every
one of them needs the apikey:manage permission - which owners and
administrators hold.
| Route | What it does |
|---|---|
GET /v1/mcp/scopes |
The permissions that can be granted, each with a sentence. |
GET /v1/mcp/connections |
This workspace's connections. Never the tokens. |
POST /v1/mcp/connections |
Mints one. The only response that carries a token. |
POST /v1/mcp/connections/{id}/revoke |
Stops it on its next request. |
POST /v1/mcp/connections/{id}/rotate |
Issues a replacement and revokes the old one at the same moment. |
GET /v1/mcp/activity |
What connections have been asked for, allowed or refused. |
GET /v1/mcp/policy |
How far assistants may go in this workspace. Needs org:read. |
PUT /v1/mcp/policy |
Changes it. |
GET /v1/mcp/actions/pending |
Proposals waiting for somebody to agree. |
POST /v1/mcp/actions/{id}/approve |
Agrees to one. Not reachable from an assistant. |
POST /v1/mcp/actions/{id}/reject |
Declines one. |
POST /v1/mcp/connections
{
"name": "Claude on my laptop",
"scopes": ["mcp:orders:read", "mcp:inventory:read"],
"lifetimeDays": 90
}
201 Created
{
"connection": {
"id": "018f4b1a-0000-7000-8000-00000000c001",
"name": "Claude on my laptop",
"tokenPrefix": "AbCdEf12",
"scopes": ["mcp:inventory:read", "mcp:orders:read"],
"effectivePermissions": ["inventory:read", "order:read"],
"canAct": false,
"createdAt": "2026-09-04T10:00:00Z",
"expiresAt": "2026-12-03T10:00:00Z",
"lastUsedAt": null,
"revokedAt": null,
"status": "ACTIVE"
},
"token": "ck_mcp_EXAMPLE_ONLY_THIS_IS_NOT_A_REAL_TOKEN"
}
Errors
The MCP endpoint answers in JSON-RPC:
| Code | Meaning |
|---|---|
-32700 | The body is not JSON. |
-32600 | Not a JSON-RPC 2.0 request. |
-32601 | No such method, or no such tool. |
-32602 |
The arguments are wrong. The message names the field. |
-32603 |
A failure, recorded, having changed nothing. |
-32001 | The request was cancelled or timed out. |
-32002 |
Not authenticated, or the connection was not granted this. |
An error message is written to be acted on and never carries an internal detail: no
database identifiers, no stack traces, no marketplace responses, no customer data.
A tool that refuses for a reason the model should read - an unknown SKU, a proposal
nobody has approved - answers with isError and a message rather than
with a protocol error.
/api/v1 and the /v1/* routes answer in RFC 9457 Problem
Details, with a correlationId you can quote to support. The
type is stable and names the kind of error:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://channekt.com/errors/not-found",
"title": "Not found",
"status": 404,
"detail": "No variant has that SKU.",
"instance": "/api/v1/inventory/changes",
"correlationId": "0a1b2c3d-..."
}
On /api/v1: 401 is a missing, unknown, revoked or expired
key; 403 is a scope the key was not given, or an address outside its
networks; 402 is a plan without the REST API; 423 is a
closed account; 400 names the field that was wrong.
Rate limits and timeouts
Requests are limited per address. The general allowance is 300 a minute; exceeding
it answers 429 with the number of seconds to wait. One tool call has
thirty seconds to finish; a client that gives up before then has its work abandoned,
and anything already queued stays queued.
A message body is capped at 256 KB, which is far more than a tool call needs.
/api/v1 is limited per key rather than per address: 120 requests a
minute for each key, so one busy integration cannot hold up another behind the same
network.
Paging
Every list is bounded at a hundred rows and returns nextCursor, or
null when there is no more. Pass the cursor back unchanged; never
construct one. The limit you asked for is echoed back, so a request
that was clamped is visible rather than silent.
A few tools page by number instead, and return page,
pageCount and total. Report the total rather than counting
the rows you were given.
Money, dates and units
An amount is an object, not a number:
{ "minor": 1799, "currency": "GBP", "decimal": "17.99" }. Compute with
minor; show decimal. When a channel charged in a different
currency, originalMinor and originalCurrency carry what it
actually charged - their absence is the statement that no conversion happened.
Dates are ISO 8601 in UTC. Weights are grams, dimensions are millimetres, and
quantities are whole numbers. available is what can be sold - on hand
less what is promised to orders - and it is the figure to quote.
Webhooks
Channekt receives webhooks from every channel you connect, so a change at a shop
reaches your catalogue without you asking. An assistant that needs to know how a
queued operation ended reads it back from
get_channel_status or list_sync_errors, which answer for
every channel at once rather than one endpoint per shop.
Versioning
The MCP protocol version is negotiated per connection. The tool catalogue itself is additive: tools and fields are added, and an existing tool's arguments are not removed or renamed without a new name beside the old one. A change that would break a caller gets a new tool.
The REST API is versioned in its path. Fields and routes are added to
/api/v1; one is removed or changes meaning only in a
/api/v2, which runs beside it. /v1/* is the web
application's own surface and carries no such promise.
The serverInfo.version returned by initialize is the build
you are talking to, which is what to quote when reporting something.