Developer Resources
API Conventions
What every endpoint of the Kaiten API shares — identifiers and slug rules, cursor pagination, the problem+json error envelope, field formats, and bringing your own identifiers.
Every operation on the Kaiten API follows the same handful of conventions. This page states them once, verified against the OpenAPI specification; the generated API reference shows how each individual operation applies them.
The basics, shared by every call:
- Base URL —
https://app.kaiten.sh/apion Kaiten Cloud;http://localhost:6060/apion the Community Edition stack (Envoy onKAITEN_PORT). The examples below use$KAITEN_APIfor it, as the quickstart does. - Authentication —
Authorization: Bearer <credential>, where the credential is aksh_service-account token or a console session. The authentication page covers issuing one; the scopes page covers what it may do. - Bodies — JSON in, JSON out. Send
Content-Type: application/jsonon every request that carries a body. - Success codes —
201with the created record for creations, except a License's grant (POST /licenses/{licenseSlug}/entitlementsanswers204);204with no body for deletions and for mostPUTupdates, so read the record back with aGETwhen you need the result;200for reads and for the operations that return a computed result — usage reports, upserts, flag evaluations, the License lifecycle transitions (publish,archive, andunarchiveanswer the License), and the two notification writes.
Identifiers: ids and slugs
Every record carries two identifiers:
id— a UUID assigned by Kaiten. It is what request bodies use to reference another record: an instance is created withcustomerId,licenseId, and optionallydeploymentZoneId.slug— a URL-friendly identifier, and what paths use to address a record:/customers/{customerSlug},/instances/{instanceSlug}/entitlements/{entitlementSlug}/usage.
Slugs obey one rule set across the API:
| Rule | Value |
|---|---|
| Pattern | ^[a-z0-9][a-z0-9-]*[a-z0-9]$ — lowercase letters, digits, and hyphens; it starts and ends with a letter or digit |
| Length | 2 to 100 characters |
| Uniqueness | Unique per organization, token slugs included |
| Generation | Optional at creation. Pass it and Kaiten validates it; omit it and Kaiten generates one, with a random suffix — see below |
| Mutability | Instances, components, and feature flags can be renamed by an update. Every other slug is fixed at creation — see Updates |
A generated slug is not the name alone. It is the name in lowercase — accents dropped, apostrophes removed, any other run of characters turned into one hyphen — followed by a hyphen and six random hexadecimal characters: a License named Omakase gets a slug such as omakase-3f9a1c, never omakase. Read the slug from the creation response before addressing the record, or send your own. Three kinds derive theirs from something other than the name: a version added to an existing License family takes {familySlug}-v{version} — standard-v3 — with no random part; a release's slug is generated from its version; a component's from its name and version.
The pattern admits no underscore, so every slug is kebab-case — entitlements and feature flags alike. The seeded example tells the two kinds apart by suffix rather than by case: the flag delivery-tracking-enabled reads the entitlement delivery-tracking. The vocabulary page documents the conventions. Pick one per kind and keep it — renaming a slug later means touching every call site.
Updates
An update replaces. The body of a PUT has the schema of the read, and the writable fields are written as sent: an optional field left out is cleared, not kept — a customer updated without its domain loses it. The fields a response carries and no request can set — id, the timestamps, the attribution, derived slugs such as customerSlug — are accepted and ignored, so a record can be read, changed, and sent back whole.
Identity fields are echo-only. They may come back in the body, with the value already stored or not at all. Any other value is refused with 422:
| Field | On an update of | Refused with |
|---|---|---|
slug | A customer, a License, an entitlement, an entitlement group, a deployment zone, a service account | <Operation>.SlugNotRenameable — UpdateCustomer.SlugNotRenameable |
version, familyId, lifecycleState | A License | UpdateLicense.VersionNotSettable, UpdateLicense.FamilyNotReassignable, UpdateLicense.LifecycleStateNotSettable |
entitlementSlug | A License's grant | UpdateLicenseEntitlement.EntitlementSlugMismatch |
key, resourceType | A metadata field — the PATCH body requires both | UpdateMetadataField.KeyImmutable, UpdateMetadataField.ResourceTypeImmutable |
Some fields are refused whenever they are present, because another operation owns them: integrations on a customer or an instance (UpdateCustomer.IntegrationsNotSettable, UpdateInstance.IntegrationsNotSettable — use the integration links), previousComponentId on a component (UpdateComponent.PreviousComponentNotSettable), and familySlug on a License (UpdateLicense.FamilyNotReassignable). A metadata field's displayOrder moves through POST /metadata-fields/reorder only.
Instances, components, and feature flags are the three kinds an update can rename, by sending a new slug. An instance or a component updated without one keeps its slug — except a component a release bundles, whose update creates its next version under a slug generated from its name and version; a feature flag's PUT writes the slug it is sent, so send it every time.
Pagination
Every top-level list but the connector registry is cursor-paginated: GET /customers, /instances, /licenses, /license-families, /entitlements, /entitlement-groups, /feature-flags, /components, /releases, /deployment-zones, /metadata-fields, /service-accounts, plus the nested lists /licenses/{licenseSlug}/entitlements, /service-accounts/{serviceAccountSlug}/tokens, and /instances/{instanceSlug}/audit-trails.
| Query parameter | Meaning |
|---|---|
limit | Maximum number of entries in the page — 1 to 200, default 50 |
cursor | The nextCursor value from the previous page. Omit it for the first page |
The response has the same shape on every one of them:
| Field | Meaning |
|---|---|
items | The records of this page |
hasMore | true while more rows exist beyond this page |
nextCursor | The opaque cursor to pass back as ?cursor=. Absent when hasMore is false |
The Kaiten Sushi Shop dataset — the demo seed profile, or the Cloud demo sandbox — has four customers, so a page of two leaves two for the next one:
curl "$KAITEN_API/customers?limit=2" \
-H "Authorization: Bearer $KAITEN_TOKEN"{
"items": [
{ "id": "…", "name": "…", "slug": "…", "…": "…" },
{ "id": "…", "name": "…", "slug": "…", "…": "…" }
],
"hasMore": true,
"nextCursor": "…"
}curl "$KAITEN_API/customers?limit=2&cursor=<the nextCursor value>" \
-H "Authorization: Bearer $KAITEN_TOKEN"{
"items": [
{ "id": "…", "name": "…", "slug": "…", "…": "…" },
{ "id": "…", "name": "…", "slug": "…", "…": "…" }
],
"hasMore": false
}The cursor is a position, not a page number: treat it as an opaque string, never build or parse one, and keep the same limit from one page to the next. Because it marks where the previous page ended rather than counting rows, records created while you walk the list cannot make a page repeat or skip rows. A handful of endpoints return the whole collection as a plain array instead — the connector registry (GET /connectors), the usage of one instance's entitlements (GET /instances/{instanceSlug}/entitlements/usage) and of an entitlement group (GET /entitlement-groups/{entitlementGroupSlug}/usage) — because their size is bounded by the registry or the contract, not by your catalog.
One list is paged differently: the notification feed. GET /v1/notifications answers { "data", "nextCursor", "unreadCount" } — no items, no hasMore, a nextCursor that is null on the last page rather than absent — and its limit runs from 1 to 50 with a default of 20. The notification paths are also the only ones with a /v1 prefix. Notifications describes it.
Errors
Every error response is an RFC 9457 problem document — Content-Type: application/problem+json — with the same schema on every operation. A request the gateway refuses before it reaches the API — no credential or one it cannot verify (401), a rate limit (429) — gets the gateway's own response instead.
| Field | Meaning |
|---|---|
type | A URI reference to documentation for the error type: the RFC section that defines the HTTP status |
title | A short summary of the problem type — stable across occurrences |
status | The HTTP status code, repeated in the body |
detail | An explanation specific to this occurrence |
instance | The request path the problem occurred on |
code | A stable, machine-readable code, formed as <Operation>.<Reason> — GetLicense.NotFound, UpdateCustomer.SlugNotRenameable; a body the schema refuses carries UNPROCESSABLE. The field to branch on in code |
errors | For validation failures: a list of { location, message, value } entries, one per offending field, with location in the form body.name or path.customerSlug |
errorId | A correlation id for the server-side log entry that holds the withheld cause — quote it when reporting a problem |
A record that does not exist in your organization:
curl "$KAITEN_API/licenses/enterprise" \
-H "Authorization: Bearer $KAITEN_TOKEN"{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"detail": "License with slug \"enterprise\" not found",
"instance": "/api/licenses/enterprise",
"code": "GetLicense.NotFound"
}A body that fails validation names each offending field:
curl -X POST "$KAITEN_API/customers" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "slug": "ramen-yokohama" }'{
"type": "https://tools.ietf.org/html/rfc4918#section-11.2",
"title": "Unprocessable Entity",
"status": 422,
"detail": "validation failed",
"instance": "/api/customers",
"code": "UNPROCESSABLE",
"errors": [
{
"message": "expected required property name to be present",
"location": "body",
"value": { "slug": "ramen-yokohama" }
}
]
}The status codes the specification declares, and what they mean here:
| Status | When |
|---|---|
400 | The request is malformed, or a value fails a rule the schema alone cannot express |
401 | No credential, or one the gateway does not recognize |
403 | A valid credential without the scope the operation requires |
404 | No such record in your organization |
409 | The request conflicts with the current state: a slug or key already in use, a record still referenced — a release that was deployed cannot be deleted — or a lifecycle rule, such as publishing a License version that is not a draft |
422 | The body does not match the schema, and errors lists each field; or the body is well-formed and asks for something the record refuses — an echo-only field changed, an archived License version assigned to an instance |
500 | An internal failure. The cause is withheld from the response and logged under errorId |
503 | A check the operation depends on could not run, and nothing was written: on Kaiten Cloud, a creation that could not be checked against what your organization's own License grants (Entitlement.VerificationUnavailable). Retry |
detail and errors describe your request; the cause of an internal failure is never echoed back, only referenced by errorId. Branch on code and status, not on the text of detail, which may change.
Field formats
- Names are
camelCasein requests and responses:createdAt,externalCustomerId,deploymentZoneId. Four surfaces usesnake_caseand the generated reference shows each field exactly as it is spelled: feature flags (event_name,default_variant,targetings), the connector registration and settings objects (settings_schema,connector_name), the integration-link objects described below (external_id,web_url,synced_at), and the event payloads (entitlement_slug,instance_id). Usage values carryevent_count. - Timestamps are RFC 3339 strings in UTC —
2026-08-01T09:00:00Z— on everycreatedAt,updatedAt, license date, and audit-trail filter (after,before). - Ids are UUID strings.
- Unset values — fields the schema declares nullable come back as
nullwhen unset: a customer without an external identifier has"externalCustomerId": null. Fields the schema marks optional may be absent altogether. - Attribution —
createdBy, andupdatedBywhere updates are tracked, hold the acting identity — a person or a service account — on customers, instances, deployment zones, license entitlements, metadata fields, components, releases, and tokens. That is the audit trail's raw material.
Bring your own identifiers
Kaiten stores the identifiers your other systems already use, so matching a record across tools needs no mapping table — the BYO-ID principle. The API exposes it at two levels.
One free-form identifier on the customer
externalCustomerId is an optional string on the customer — the record id in your CRM or billing tool. Set it at creation or update, read it back on every customer response. In the seeded example, Sakura Tokyo carries crm-1024, Ninja Osaka crm-2048, and Kappa Kyoto crm-4096; Demo Restaurant has none. A new customer takes its own:
curl -X POST "$KAITEN_API/customers" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Ramen Yokohama", "externalCustomerId": "crm-8192" }'The field is carried, not resolved: there is no lookup by externalCustomerId. Resolution is what integration links are for.
Integration links, one per adapter
An integration link binds a customer or an instance to its record in one external system — the adapter — and is the identity the resolve-by-external-id endpoints key on. It carries external_id (required), web_url (a deep link into the other tool), metadata (adapter-specific), and last_error / synced_at (synchronization state). A record holds at most one link per adapter, and within your organization one (adapter, external_id) pair resolves to exactly one record.
Manage links on the record, addressed by the full adapter name:
| Method | Path | Scope |
|---|---|---|
POST / GET / PUT / DELETE | /customers/{customerSlug}/integrations/{integrationName} | write:customers (read:customers for GET) |
POST / GET / PUT / DELETE | /instances/{instanceSlug}/integrations/{integrationName} | write:instances (read:instances for GET) |
curl -X POST "$KAITEN_API/customers/sakura-tokyo/integrations/kaiten.integration.crm.attio" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_id": "rec_12345",
"web_url": "https://app.attio.com/w/kaiten-sushi-shop/record/rec_12345"
}'Links can also be attached at creation — POST /customers and POST /instances accept an integrations object keyed by adapter name — and every customer and instance response carries its links under integrations.
Resolve or upsert by your identifier, addressed by the adapter without its kaiten.integration. prefix:
| Method | Path | What it does |
|---|---|---|
GET | /integration/{adapter}/customer/{externalId} | Returns the customer linked to that external id, 404 if none |
PATCH | /integration/{adapter}/customer/{externalId} | Creates the customer and its link if none exists, otherwise partially updates it — fields you omit are left unchanged |
GET | /integration/{adapter}/instance/{externalId} | Returns the instance linked to that external id |
PATCH | /integration/{adapter}/instance/{externalId} | Creates or partially updates the instance; customerExternalId names its customer by that customer's external id in the same adapter |
curl -X PATCH "$KAITEN_API/integration/crm.attio/customer/rec_12345" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Sakura Tokyo", "domain": "sakura-tokyo.example" }'The PATCH is what makes a synchronization into Kaiten idempotent: the same call, replayed, converges on the same record instead of creating a duplicate, and the identifier the other system already holds is the only key either side needs. The built-in Attio connector does not use it: it runs in the other direction, Kaiten to Attio, and reads nothing back. What it writes in Kaiten is the integration link — on each Customer and each Instance it mirrors, a link under kaiten.integration.crm.attio holding the Attio record's id, its web_url, and the last synchronization error.
Webhook signatures
Kaiten CloudOutbound webhooks on Kaiten Cloud are signed on delivery, and your endpoint should verify the signature before trusting a payload — the procedure and the headers are on Verifying signatures. The Community Edition consumes the event stream directly from its own broker, where there is no delivery signature to check; the two paths are compared on events and webhooks.
Where to go next
Authentication
Service accounts and the ksh_ token lifecycle — the credential every call above carries.
Data model
The exact reference — every column, enum, and constraint the conventions above apply to.
API reference
Every operation, generated from the specification, with its parameters and response schemas.
Scopes
The permission grammar for API tokens — modules, actions, wildcards, and what each one gates.
SDKs
The official Kaiten SDKs — the Go client, the Python client, and the two @kaitencloud TypeScript packages — how each one is built, what it covers, which credential it carries, and OpenFeature for flag evaluation. Setup lives in each repository.