Kaiten

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/api on Kaiten Cloud; http://localhost:6060/api on the Community Edition stack (Envoy on KAITEN_PORT). The examples below use $KAITEN_API for it, as the quickstart does.
  • Authentication — Authorization: Bearer <credential>, where the credential is a ksh_ 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/json on every request that carries a body.
  • Success codes — 201 with the created record for creations, except a License's grant (POST /licenses/{licenseSlug}/entitlements answers 204); 204 with no body for deletions and for most PUT updates, so read the record back with a GET when you need the result; 200 for reads and for the operations that return a computed result — usage reports, upserts, flag evaluations, the License lifecycle transitions (publish, archive, and unarchive answer 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 with customerId, licenseId, and optionally deploymentZoneId.
  • 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:

RuleValue
Pattern^[a-z0-9][a-z0-9-]*[a-z0-9]$ — lowercase letters, digits, and hyphens; it starts and ends with a letter or digit
Length2 to 100 characters
UniquenessUnique per organization, token slugs included
GenerationOptional at creation. Pass it and Kaiten validates it; omit it and Kaiten generates one, with a random suffix — see below
MutabilityInstances, 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:

FieldOn an update ofRefused with
slugA customer, a License, an entitlement, an entitlement group, a deployment zone, a service account<Operation>.SlugNotRenameable — UpdateCustomer.SlugNotRenameable
version, familyId, lifecycleStateA LicenseUpdateLicense.VersionNotSettable, UpdateLicense.FamilyNotReassignable, UpdateLicense.LifecycleStateNotSettable
entitlementSlugA License's grantUpdateLicenseEntitlement.EntitlementSlugMismatch
key, resourceTypeA metadata field — the PATCH body requires bothUpdateMetadataField.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 parameterMeaning
limitMaximum number of entries in the page — 1 to 200, default 50
cursorThe nextCursor value from the previous page. Omit it for the first page

The response has the same shape on every one of them:

FieldMeaning
itemsThe records of this page
hasMoretrue while more rows exist beyond this page
nextCursorThe 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.

FieldMeaning
typeA URI reference to documentation for the error type: the RFC section that defines the HTTP status
titleA short summary of the problem type — stable across occurrences
statusThe HTTP status code, repeated in the body
detailAn explanation specific to this occurrence
instanceThe request path the problem occurred on
codeA 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
errorsFor validation failures: a list of { location, message, value } entries, one per offending field, with location in the form body.name or path.customerSlug
errorIdA 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:

StatusWhen
400The request is malformed, or a value fails a rule the schema alone cannot express
401No credential, or one the gateway does not recognize
403A valid credential without the scope the operation requires
404No such record in your organization
409The 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
422The 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
500An internal failure. The cause is withheld from the response and logged under errorId
503A 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 camelCase in requests and responses: createdAt, externalCustomerId, deploymentZoneId. Four surfaces use snake_case and 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 carry event_count.
  • Timestamps are RFC 3339 strings in UTC — 2026-08-01T09:00:00Z — on every createdAt, updatedAt, license date, and audit-trail filter (after, before).
  • Ids are UUID strings.
  • Unset values — fields the schema declares nullable come back as null when unset: a customer without an external identifier has "externalCustomerId": null. Fields the schema marks optional may be absent altogether.
  • Attribution — createdBy, and updatedBy where 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.

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:

MethodPathScope
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:

MethodPathWhat 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 Cloud

Outbound 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

On this page