Authentication and scopes

API key headers, the bearer token flow, scopes and channel bindings.

Every request to the Channel API is authenticated at Kentro's gateway before it reaches the API. The gateway accepts an API key pair, a bearer token minted from that pair, or a single-token key; it resolves the key's tenant, scopes and channel bindings and refuses anything else with 401.

Headers

Send one of these on every request.

MethodHeadersWhen to use it
Key pairkentro-key: <key> and kentro-secret: <secret>The default for server-to-server integrations. The pair is what the OS portal shows when a key is created.
Bearer tokenAuthorization: Bearer <accessToken>When requests are made from a client that should not hold the secret, or when many requests per second make the per-request credential check worth skipping. See the token flow below.
Single tokenX-API-Key: <token>Keys issued as one token instead of a pair. The gateway compares the token against every active key of the platform, so prefer the pair or a bearer token for high request rates.

The secret is a credential. Keep it in a secrets store, never in a URL, a log line or a client-side bundle.

The bearer token flow

Exchange the key pair for a token at the authentication service. The access token lives 15 minutes; the refresh token rotates on every use.

curl -s -X POST https://apis.kentro.live/auth/login/api-key \
  -H "Content-Type: application/json" \
  -d '{ "key": "'"$KENTRO_KEY"'", "secret": "'"$KENTRO_SECRET"'" }'
{
  "accessToken": "eyJhbGciOi...",
  "refreshToken": "4f2c7a...",
  "expiresIn": 900,
  "user": { "id": "...", "tenantId": "...", "type": "api-key" }
}

Send Authorization: Bearer <accessToken> on Channel API calls. Before expiresIn seconds pass, rotate:

curl -s -X POST https://apis.kentro.live/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "'"$REFRESH_TOKEN"'" }'

The answer has the same shape with a new pair of tokens. A refresh token used twice is refused. On the sandbox the host is https://apis.kentro.dev.

A single-token key logs in with { "apiKeyRaw": "<token>" } instead of key and secret.

Scopes

A key holds the scopes of its permission groups. Each endpoint on this reference states the scope it requires under Scope. admin grants everything. channel:write grants every write scope and channel:read. A write scope grants the writes of its resource and the reads every scope includes.

ScopeGrants
channel:readGET on every resource: channels, customers, products, catalog, inventory, orders, fulfillment orders, the fulfillment queue, shipments, invoices, payments, purchase orders, vendors, operations.
channel:writeEverything below, plus channel:read.
customers:writeCreate, update and delete customers; customer credits.
products:writeCreate, update and delete products; identifiers, listings, price lists, inventory levels, product import.
orders:writeCreate, update, cancel and delete orders; stock review decisions; invoices of an order; order import.
fulfillment:writeCreate, generate, update, fulfill, reroute, cancel and delete fulfillment orders.
shipments:writeRecord, update, cancel and delete shipments; ship from the fulfillment queue.
invoices:writeCreate, update, cancel and delete invoices.
payments:writeCreate, update, apply, unapply, cancel, decline and delete payments.
purchase-orders:writeCreate, update, accept, reject, cancel and delete purchase orders.

Permission groups created before these scopes existed keep working: orders:manage counts as orders:write, invoices:manage as invoices:write, payments:manage as payments:write and fulfillment-orders:manage as fulfillment:write. Reads need channel:read, channel:write or admin.

A refused scope is 403 with code: forbidden and a message naming the scope:

{
  "error": {
    "code": "forbidden",
    "message": "This operation requires the orders:write scope",
    "requestId": "3674fefa-5f01-48db-a482-1ad801fbe720"
  }
}

Channel bindings

A key can be bound to one or more sales channels when it is created or edited. A bound key may write only into those channels: a document naming another channelId is 403 with detail code not_bound. Reads are not restricted by the binding. An unbound key may write into any channel of its tenant.

A channelId that belongs to another tenant, or that does not exist, is 404 not_found (422 unresolved_reference when it is named inside a body). Nothing a key does can reach another tenant.

The tenant

The key's tenant is the tenant of every request. There is no header, body field or query parameter that selects a tenant: a key made in tenant A reads and writes tenant A.

What the gateway answers

Failures before the request reaches the API use the gateway's own shape, not the API's envelope:

StatusBodyCause
401{ "error": "INVALID_REQUEST", "message": "Missing API key credentials" }No credentials.
401{ "error": "INVALID_CREDENTIALS", ... }Unknown key, wrong secret, revoked or expired key.
401{ "error": "TOKEN_EXPIRED", ... }The bearer token is past its 15 minutes: refresh.
403{ "error": "TENANT_INACTIVE", ... }The tenant is deactivated.

Once past the gateway, every refusal uses the error envelope.