Make your first request
A first Channel API call, a dry-run write and a first Store API call against the sandbox, in cURL and Node.
This page takes a sandbox API key to a first answer from each API. The examples use the sandbox hosts; in production, swap .dev for .live.
Before you start
- Access to a tenant in the sandbox at
https://kentro.cc. Questions about access go to [email protected]. - For the Channel API: an API key whose permission group holds
channel:read, pluscustomers:writefor the dry run. See API keys and scopes. - For the Store API: an API key whose permission group holds
storefront:serve, and the host of a published storefront. - For the Node examples: Node.js 18 or later (built-in
fetch). Save each example as a.mjsfile so top-levelawaitworks, and run it withnode <file>.mjs.
Export the values once:
export KENTRO_KEY="<key from the API keys dialog>"
export KENTRO_SECRET="<secret from the API keys dialog>"
export STOREFRONT_HOST="shop.example.com"
1. Check reachability
curl -s https://apis.kentro.dev/channel/v1/ping
{ "ok": true, "service": "channel-api" } means the host answers. The ping takes no credentials.
2. A first Channel API call
List the tenant's sales channels. The ids are the channelId values every Channel API document carries.
curl -s https://apis.kentro.dev/channel/v1/channels \
-H "kentro-key: $KENTRO_KEY" \
-H "kentro-secret: $KENTRO_SECRET"
const res = await fetch('https://apis.kentro.dev/channel/v1/channels', {
headers: {
'kentro-key': process.env.KENTRO_KEY,
'kentro-secret': process.env.KENTRO_SECRET,
},
});
const body = await res.json();
if (!res.ok) {
console.error(res.status, JSON.stringify(body), res.headers.get('x-request-id'));
process.exit(1);
}
for (const channel of body.data) console.log(channel.id, channel.type, channel.name);
The answer:
{
"data": [
{
"id": "64f0aabb-ccdd-1122-3344-000100000000",
"name": "Web store",
"type": "woocommerce",
"isActive": true,
"currency": "USD"
}
],
"nextCursor": null,
"limit": 50
}
nextCursor: null means this is the last page. Lists are cursor based and newest first; see Channel API: Requests and responses.
3. A first write, as a dry run
X-Dry-Run: true runs a write in full, answers what it would have produced, and rolls everything back: nothing is stored and no event is recorded. Set CHANNEL_ID to an id from step 2.
curl -s -i -X POST https://apis.kentro.dev/channel/v1/customers \
-H "kentro-key: $KENTRO_KEY" \
-H "kentro-secret: $KENTRO_SECRET" \
-H "Idempotency-Key: first-customer-0001" \
-H "X-Dry-Run: true" \
-H "Content-Type: application/json" \
-d '{
"channelId": "'"$CHANNEL_ID"'",
"externalId": "first-customer-0001",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"onDuplicate": "update"
}'
const res = await fetch('https://apis.kentro.dev/channel/v1/customers', {
method: 'POST',
headers: {
'kentro-key': process.env.KENTRO_KEY,
'kentro-secret': process.env.KENTRO_SECRET,
'Idempotency-Key': 'first-customer-0001',
'X-Dry-Run': 'true',
'Content-Type': 'application/json',
},
body: JSON.stringify({
channelId: process.env.CHANNEL_ID,
externalId: 'first-customer-0001',
firstName: 'Ada',
lastName: 'Lovelace',
email: '[email protected]',
onDuplicate: 'update',
}),
});
console.log(res.status, res.headers.get('x-dry-run'), JSON.stringify(await res.json(), null, 2));
A 201 with the response header X-Dry-Run: applied and "outcome": "created" in the body shows the request is valid. Without the X-Dry-Run header, the same request creates the customer; sent again, it answers 200 with outcome: updated or unchanged, because onDuplicate: update makes the create an upsert.
4. A first Store API call
store.get reads the published storefront behind a host. It needs the API key and the host, nothing else. X-API-Key carries the key's secret.
curl -s -X POST https://apis.kentro.dev/store/v1/store.get \
-H "X-API-Key: $KENTRO_SECRET" \
-H "X-Storefront-Host: $STOREFRONT_HOST" \
-H "Content-Type: application/json" \
-d '{}'
const res = await fetch('https://apis.kentro.dev/store/v1/store.get', {
method: 'POST',
headers: {
'X-API-Key': process.env.KENTRO_SECRET,
'X-Storefront-Host': process.env.STOREFRONT_HOST,
'Content-Type': 'application/json',
},
body: '{}',
});
const store = await res.json();
if (!res.ok) {
console.error(res.status, JSON.stringify(store));
process.exit(1);
}
console.log(store.name, store.access, store.currency, store.guestCheckout);
An excerpt of the answer:
{
"id": "0b6c2e51-5f1e-4a4f-8d0e-6a3d5c2f9a10",
"name": "Example Wholesale",
"access": "approval",
"guestCheckout": false,
"publicPrices": false,
"paymentProvider": "stripe",
"fulfillment": ["shipping", "pickup"],
"currency": "USD"
}
access, publicPrices, guestCheckout and currency decide what a storefront renders before anyone signs in. Every amount the Store API answers is an integer in the minor unit of currency. See Store API: Getting started.
When the first call fails
| Answer | Cause | Fix |
|---|---|---|
401 { "error": { "code": "unauthenticated", ... } } | Channel API: no credentials reached the API. | Send kentro-key and kentro-secret. |
401 { "error": "INVALID_REQUEST", ... } | Store API: no X-API-Key. | Send the key's secret as X-API-Key. |
401 { "error": "INVALID_CREDENTIALS", ... } | Wrong or disabled key, wrong secret, or a key from the other environment. | Check the values and the host. |
403 { "error": { "code": "forbidden", ... } } | The key lacks the scope named in message. | Add the scope to the key's permission group. |
403 { "code": "FORBIDDEN", "message": "This API key cannot serve a storefront." } | The key's groups hold neither storefront:serve nor admin. | Add storefront:serve. |
400 { "code": "VALIDATION", ... } on store.get | X-Storefront-Host is missing. | Send the published host. |
404 { "code": "NOT_FOUND", "message": "No published storefront answers this host." } | The storefront is not published, or the host belongs to the other environment. | Publish it, or check the host. |
Next: Errors, retries and rate limits, then the integration guide for what you are building, starting from the developer center.
Updated 1 day ago

