Usage guidelines
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.
Reads are open, writes are actions
Section titled “Reads are open, writes are actions”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:
| Route | What it returns |
|---|---|
GET /v1/object-types | The 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}/search | A 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}/traverse | A walk out from one object across several link types. |
GET /v1/object-types/{type}/objects/{id}/as-of | The object as it was at a point in time, rebuilt from the event log. |
Every action has four routes
Section titled “Every action has four routes”For a registered action hospitality.action.check_in:
| Route | Scope | What happens |
|---|---|---|
POST …/check_in/validate | objects:read | Checks 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/preview | the apply scope | Executes 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/whatif | the apply scope | A 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/apply | the apply scope | The 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.
Idempotency keys
Section titled “Idempotency keys”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
422and the codeidempotency_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
409with the codeidempotency_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.
Versions, ETags and conflicts
Section titled “Versions, ETags and conflicts”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.
The error envelope
Section titled “The error envelope”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.
| Status | Meaning |
|---|---|
401 | No bearer token, or an invalid one. |
403 | The token lacks the scope this route needs. |
404 | No such object, type, or action. |
409 | A conflict: a business rule refused the change, or an idempotency key is in flight. |
422 | The parameters failed validation, or an idempotency key was reused with a different body. |
429 | Rate limited. |
5xx | Our 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.
Pagination
Section titled “Pagination”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.
Rate limits
Section titled “Rate limits”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.
Time, money and identifiers
Section titled “Time, money and identifiers”- 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_datefunction 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_bookingfunction looks one up.
- Events and webhooks: consume what changed.
- Authentication: scopes and service accounts.
- API reference: every action’s parameter schema.