Skip to content

Usage guidelines

View as Markdown

Everything on this page is a property of the kernel, not of any one product. The same rules hold for the REST API, the generated SDKs, and the MCP tools, which call the same engine.

There is no generic update call. A read returns an object with its version; a write is a named, validated action applied to a target, and the engine mutates the affected objects and links atomically and appends an event. If you are looking for PUT on an object, the answer is the action that makes that change.

Reads, by object type:

RouteWhat it returns
GET /v1/object-typesThe registry: every object type, with its properties and the actions that can change it.
GET /v1/object-types/{type}/objects/{id}One object, with its version. The response carries a strong ETag of that version and Cache-Control: no-store.
POST /v1/object-types/{type}/searchA filtered, paginated query over one type.
GET /v1/object-types/{type}/objects/{id}/links/{link_type}The objects linked to this one by a link type.
GET /v1/object-types/{type}/objects/{id}/traverseA walk out from one object across several link types.
GET /v1/object-types/{type}/objects/{id}/as-ofThe object as it was at a point in time, rebuilt from the event log.

For a registered action hospitality.action.check_in:

RouteScopeWhat happens
POST …/check_in/validateobjects:readChecks the parameters against the action’s schema and shape: target present and of the right type, no unknown parameters. No handler runs.
POST …/check_in/previewthe apply scopeExecutes the action and rolls it back, returning the objects and links it would modify. Nothing persists and no outward effect fires. The response is marked "preview": true.
POST …/check_in/whatifthe apply scopeA preview that also folds the would-be events into the rooms rollup, so you see the change to occupancy, ADR and RevPAR before committing.
POST …/check_in/applythe apply scopeThe write. Validates, applies, appends events, fires effects.

The request body is the same shape on all four:

{
"target": { "object_type": "hospitality.reservation", "id": "0192f3c4-…" },
"params": { "arrival_actual": "2026-10-31T15:42:00Z" },
"return_modified": true
}

target is absent for actions that create a new object. return_modified asks for the modified_objects and modified_links projections in the response; it defaults to false to keep the common case small.

A successful apply returns:

{
"correlation_id": "6f1c…",
"modified_objects": [ { "id": "…", "object_type": "hospitality.stay", "kind": "created" } ],
"modified_links": [ { "link_type": "hospitality.link.stay_in_room", "from_type": "…", "from_id": "…", "to_type": "…", "to_id": "…", "kind": "linked" } ],
"warnings": []
}

correlation_id is stamped on every event this invocation appended, so you can find them later with GET /v1/events?correlation_id=…. warnings carries non-blocking observations from the action’s soft validators.

The apply scope is actions:{api_name}:apply, or the catch-all actions:apply. See Authentication.

Every side-effecting POST accepts an Idempotency-Key header. Send a fresh UUID per logical operation, and reuse it on a retry.

  • The first call with a key runs and its response is cached for 24 hours.
  • A retry with the same key and the same request replays the cached response. The side effects do not run again.
  • The same key with a different body is refused with 422 and the code idempotency_key_mismatch. The message names the operation the key was first used for.
  • A retry that arrives while the first call is still running gets 409 with the code idempotency_in_flight. Two retries never both run the side effects.
  • After 24 hours a key is a fresh request.

The generated SDKs expose the header as a parameter on every write method.

Every object carries a monotonic version that increments on each change, and an object read returns it as a strong ETag. Actions read the current state of their target when they run, under the engine’s own locking, so two callers cannot both apply a conflicting change: the second gets 409 with the reason. There is no version to echo back on a write, because there is no generic write. Hold the version you read to tell whether an object has moved since; apply the action to make it move.

Every error, from any route, has one shape:

{
"error": {
"code": "unauthorized",
"message": "missing bearer token",
"instance_id": "01HZ…",
"parameters": {}
}
}

The HTTP status says what class of failure it is. The engine never returns 200 for a failed action.

StatusMeaning
401No bearer token, or an invalid one.
403The token lacks the scope this route needs.
404No such object, type, or action.
409A conflict: a business rule refused the change, or an idempotency key is in flight.
422The parameters failed validation, or an idempotency key was reused with a different body.
429Rate limited.
5xxOur fault. The instance_id is what to send us.

instance_id matches the request-id response header, so a support conversation can find the exact request without scraping logs. code is stable and machine-readable; message is for people and may change.

List routes return a page and a next_cursor. Pass it back as cursor to continue. Treat the cursor as opaque: it is not a page number and is not meant to be built by hand. The event log also accepts since, a numeric sequence number, for consumers that keep their own position. See Events and webhooks.

A per-IP token bucket governs the API. The defaults are 60 requests per second steady state with a burst of 100, and 5 requests per second on POST /oauth/token, because each token request verifies a secret. Over the limit you get 429. Cache tokens for their lifetime rather than fetching one per request, and back off on 429 before retrying.

  • Timestamps are RFC 3339 in UTC. The hotel’s business date is a separate notion that the night audit advances; read it with the hotel_date function rather than deriving it from the clock.
  • Money is an { "amount": "120.00", "currency": "GBP" } pair. Amounts are decimal strings, never floats.
  • Object ids are UUIDs. Business ids such as a booking number are properties on the object, and the find_booking function looks one up.