Requests and responses

Ids, money, dates, envelopes, duplicates, pagination, idempotency, errors, rate limits, dry runs and historical imports.

Every Channel API endpoint follows the same rules. Learn them once here; the endpoint reference assumes them.

Ids

Every document has a Kentro id, a uuid. Three forms address a document wherever a path takes :id or a body takes an id:

FormExampleNotes
uuid6512aa00-bb11-cc22-dd33-ee4400000000What every response carries.
legacy 24-hex id6512aa00bb11cc22dd33ee44The objectId of the legacy Channel API; the same record.
ext:<channelId>:<externalId>ext:64f0aabb-ccdd-1122-3344-000100000000:shop-1001Your own id for the document inside one sales channel.

externalId is the integration's own identifier for a document, unique within its sales channel. Send it on every create: it is what makes onDuplicate find the document again, and what ext: addressing resolves.

Money

Money travels as decimal strings in currency units with an explicit or implied currency: "unitPrice": "12.50" with "currency": "USD". Numbers are accepted on input and treated the same way. An amount with more decimals than its currency allows is 400 validation_failed with detail code precision. Responses always use strings. There are no cents anywhere on the wire.

Totals are what the channel charged: subtotal, shipping, salesTax, dutyTax, vat, discount, total, paid, refunded. The API checks that they add up; a mismatch is 422 totals_mismatch with the sent and computed amounts in details.

Dates

Timestamps are ISO-8601 in UTC (2026-11-08T14:03:00Z). Date-only fields (dueDate, shipNotBefore, purchaseOrderDate) accept YYYY-MM-DD, anchored at noon UTC so the calendar day survives every time zone. externalCreatedAt and externalUpdatedAt record when the document happened on the channel; createdAt and updatedAt are Kentro's.

Envelopes

RequestAnswer
GET /v1/<resource>/:id200 { "data": { ... } }
GET /v1/<resource>200 { "data": [ ... ], "nextCursor": "..." or null, "limit": 50 }
POST, PATCH, DELETE, actions201 or 200 { "data": { ... }, "outcome": "...", "warnings"?: [ ... ] }
Bulk import (POST /v1/orders/import, POST /v1/products/import)202 { "data": { "operationId": "..." } }, then GET /v1/operations/:id
Any failure{ "error": { "code", "message", "details"?, "requestId" } }

outcome is one of created, updated, skipped, deleted, unchanged. unchanged means the write found nothing to change and wrote nothing, so it emitted no events either. warnings lists what the write accepted with a caveat (product_twins, unresolved_product, reservation_short, ...); each carries a code, a message and the ids involved.

Duplicates: onDuplicate

A POST that finds the document it describes (by id, by externalId in the channel, and for customers by email then phone) does what onDuplicate says:

onDuplicateAnswer when the document exists
update (default)200 updated or 200 unchanged: the create body is applied as an update.
skip200 skipped: the stored document is answered, nothing written.
error409 duplicate with the matching field in details.

So a create is an upsert by default. An integration that replays its full feed can POST every document every time.

Partial updates and version

PATCH /v1/<resource>/:id applies the fields sent and leaves the rest. Every document carries version, an opaque optimistic-concurrency token. Send it back as version in the PATCH body to refuse the update if the document changed in between (409 version_conflict); leave it out to update unconditionally.

Pagination

Lists are cursor based and never count totals. limit is 1 to 200 (default 50). The answer's nextCursor is the cursor of the next request; null means the last page. Order is newest first, stable across pages. Lists filter by channelId, externalId, status, createdSince, updatedSince and resource-specific fields; the reference lists each one.

updatedSince is the field for incremental sync: pass the latest updatedAt seen so far.

GET /v1/<resource>/:id expands every relation (include=items,customer on orders); a list expands none unless include asks for it.

Idempotency

Every POST, PUT, PATCH and DELETE takes an Idempotency-Key header: any string of 7 to 50 characters that identifies the request on your side (shop-1001-create, a webhook delivery id). The first request with a key is executed and its answer stored for 24 hours.

RetryAnswer
Same key, same bodyThe stored answer, with Idempotent-Replayed: true.
Same key, different body422 idempotency_key_reused.
Same key while the first is still running409 in_progress with Retry-After.

Keys are scoped per tenant and per endpoint. A request that failed is not stored: a retry with the same key runs again. The legacy query parameter ?idempotency= is accepted as an alias.

Errors

Every refusal past the gateway is JSON with one error object. details, when present, is a list of { path, code, message } naming the field.

{
  "error": {
    "code": "validation_failed",
    "message": "Request validation failed",
    "details": [
      { "path": "items[0].quantity", "code": "minimum", "message": "must be >= 1" }
    ],
    "requestId": "2f408915-2608-4f0e-90a7-2a7abd34f487"
  }
}
StatuscodeMeaning
400validation_failedA field is missing, malformed or unknown. Unknown fields are refused, never dropped.
401unauthenticatedNo usable identity reached the API.
403forbiddenThe key lacks the scope, or writes into a channel it is not bound to (not_bound).
404not_foundNo document at that id in this tenant.
409duplicateonDuplicate: error and the document exists.
409state_conflictThe document's state forbids the action (cancel a shipped order, delete a product that holds stock). details[0].code names the case.
409in_progressThe same Idempotency-Key is being processed.
409version_conflictThe version sent is stale.
422totals_mismatchThe totals do not add up to the lines and charges.
422inactive_customerThe customer is inactive.
422inventory_unavailableThe stock the action needs is not there.
422unresolved_referenceA referenced document does not exist: an unknown SKU (not_found), a product group instead of a sellable variant (group_container), an unknown warehouse.
422idempotency_key_reusedThe key was used with a different body.
429rate_limitedOver the tenant's rate limit; Retry-After says when to retry.
503route_disabledKentro paused this endpoint; retry later.
503dependency_unavailableA dependency was briefly unavailable; retry with the same Idempotency-Key.
500internalReported to Kentro with the requestId.

requestId is also the X-Request-Id response header. Quote it when contacting support.

Rate limits

Limits are per tenant and per minute: 1,200 writes (POST, PUT, PATCH, DELETE) and 6,000 reads (GET) by default. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). A refusal is 429 rate_limited with Retry-After. Higher limits are set per tenant by Kentro.

Dry runs and historical imports

Three optional request headers change how a write is applied. They are for integrations and bulk loads; a value outside the accepted set is 400 validation_failed.

HeaderEffect
X-Dry-Run: trueThe write runs in full and answers the status and body it would have produced, with X-Dry-Run: applied, then everything is rolled back. Nothing is stored, no event is emitted, no idempotency result is kept.
X-Import-Mode: historicalThe document is stored without raising the downstream effects a live document has: no confirmation email, packing slip, accounting export or EDI document. For loading a channel's history. Pair it with fulfillment.skip and per-line fulfilledQuantity on orders, and shippedAt on shipments.
X-Customer-Match: externalIdCustomers are matched by id and externalId only, never by email or phone, so two source records that share an email stay two customers.

Asynchronous operations

POST /v1/orders/import and POST /v1/products/import accept up to a batch of documents and answer 202 with an operationId. GET /v1/operations/:id answers status (queued, running, succeeded, failed, canceled) and, once finished, the per-document results; GET /v1/operations/:id/input answers the batch as it was received.