# Usage guidelines

> How to call the API well. The four routes every action has, idempotency keys, versions and ETags, the error envelope, pagination and rate limits.

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

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

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:

```json
{
  "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:

```json
{
  "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](/guides/authentication/).

## 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 `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.

## 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

Every error, from any route, has one shape:

```json
{
  "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

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](/guides/events-and-webhooks/).

## 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

- 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.

## Next

- [Events and webhooks](/guides/events-and-webhooks/): consume what changed.
- [Authentication](/guides/authentication/): scopes and service accounts.
- [API reference](/api/): every action’s parameter schema.

---

Source: https://developers.aerandil.com/guides/usage-guidelines/
