> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kayanos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Public API reference

> Connect a server-side integration to explicitly enabled KayanOS records with scoped access, pagination, and safe concurrent updates.

## What this API is for

The KayanOS public API is a versioned, server-to-server interface for records that an organization has deliberately made available. It is appropriate for a controlled integration such as a service-centre reporting system that reads permitted **Citizen Service Request** records or updates an approved record through a managed service account.

It is not a way to bypass KayanOS roles, scopes, field visibility, approval rules, or a form's public portal. A token acts as its owning member: the API can only perform the entity verbs and see the record/field data that the owner is allowed to use. Treat every token as a high-value credential.

![KayanOS public API safety guidance without credentials.](https://kayanos.app/docs-images/en/reference/public-api.png)

## Before you send a request

An organization owner or an administrator with the relevant API-token permission should complete this checklist:

1. In the entity configuration, enable the entity for the public API. An entity that is not enabled returns `ENTITY_NOT_FOUND` through this interface, even if it exists internally.
2. Give the token owner only the required entity verbs: `list`, `create`, `update`, and/or `delete`. Scope assignment still applies to actual records.
3. Review fields. A field protected by a read verb is omitted unless the token owner has that verb. Protected fields are also not valid filter or sort fields through v1.
4. Create a named token with an owner, purpose, expiry decision, and rotation/revocation owner. Store the one-time secret in an approved secret manager, not in browser code, a form, a document template, a ticket, or a screenshot.
5. Start with a read-only request against an approved non-production record with no personal data. Record the returned `x-request-id` with the integration test evidence.

The token format begins with `kayan_v1_`; this is only an identifier format, not a permission grant. A valid-looking token can still be expired, revoked, owned by an inactive member, or unable to access an entity.

## Base URL, version, and authentication

Use the KayanOS host for the target organization and the v1 base path:

```bash theme={null}
export KAYANOS_API_BASE_URL="https://<your-kayanos-host>/api/v1"
export KAYANOS_API_TOKEN="kayan_v1_<token-id>_<secret>"
```

Every request uses a Bearer token. Do not put the token in a URL or query string.

```bash theme={null}
curl --request GET "$KAYANOS_API_BASE_URL/me" \
  --header "Authorization: Bearer $KAYANOS_API_TOKEN" \
  --header "Accept: application/json" \
  --header "X-Request-Id: csr-integration-check-001"
```

The server returns the supplied request ID, or creates one when it is absent. Keep it when reporting a failure. Successful payloads use a `data` envelope; errors use an `error` envelope.

```json theme={null}
{
  "data": {
    "organizationId": "…",
    "member": { "id": "…", "name": "Integration owner", "email": null },
    "token": { "id": "…", "name": "service-centre-reporting", "tokenHint": "…" }
  }
}
```

```json theme={null}
{
  "error": {
    "code": "PERMISSION_DENIED",
    "message": "This token cannot access the entity.",
    "requestId": "csr-integration-check-001"
  }
}
```

## Endpoint map

| Method   | Path                                     | Use it for                                                                                    |
| -------- | ---------------------------------------- | --------------------------------------------------------------------------------------------- |
| `GET`    | `/me`                                    | Confirm the organization, token identity, and owner before an integration runs.               |
| `GET`    | `/entities`                              | Discover API-enabled entities that have at least one usable verb for this token.              |
| `GET`    | `/entities/:entityKey`                   | Inspect the entity's visible fields and the token's `list/create/update/delete` capabilities. |
| `GET`    | `/entities/:entityKey/records`           | List a page of readable records.                                                              |
| `POST`   | `/entities/:entityKey/records/query`     | List with a JSON filter, sort, cursor, and search body.                                       |
| `GET`    | `/entities/:entityKey/records/:recordId` | Read one record and receive its `ETag`.                                                       |
| `POST`   | `/entities/:entityKey/records`           | Create one record from a `{ "data": { … } }` body.                                            |
| `PATCH`  | `/entities/:entityKey/records/:recordId` | Update one record with a current `If-Match` value.                                            |
| `DELETE` | `/entities/:entityKey/records/:recordId` | Delete one record with a current `If-Match` value.                                            |

Replace `:entityKey` with the entity key returned by discovery, not a translated display title. Replace `:recordId` with the API record ID; an internal entity-prefix, if present, is not required in this URL.

## Discover the actual contract first

Do not guess field keys or assume that an internal entity is exposed. Start with discovery and save the result with the integration configuration.

```bash theme={null}
curl "$KAYANOS_API_BASE_URL/entities/service_requests" \
  --header "Authorization: Bearer $KAYANOS_API_TOKEN"
```

The result includes the entity's visible field metadata and a permission map. A `false` permission means the integration must not attempt that verb; it is not a signal to retry with a broader filter. When a field has a read restriction, it may be absent from the field list and cannot be used in the public API's filtering or sorting.

## List, search, filter, and paginate records

`GET` listing accepts `limit`, `cursor`, `sort`, and `search` query parameters. The default page size is 100 and the maximum is 500. The default sorting is newest-first by `created`. Query-string sorting uses comma-separated `field:direction` values.

```bash theme={null}
curl "$KAYANOS_API_BASE_URL/entities/service_requests/records?limit=25&sort=created:desc,request_id:asc&search=CSR-2026" \
  --header "Authorization: Bearer $KAYANOS_API_TOKEN"
```

The response has `items` and `nextCursor`. Treat `nextCursor` as opaque: send it back unchanged with the same logical query. Do not manufacture, decode for business logic, or reuse a cursor with a changed sort/filter.

```json theme={null}
{
  "data": {
    "items": [
      {
        "id": "9d…",
        "entityKey": "service_requests",
        "created": "2026-07-12T09:30:00.000Z",
        "updated": "2026-07-12T09:32:00.000Z",
        "versionId": "17",
        "data": { "request_id": "CSR-2026-00042", "status": "closed" }
      }
    ],
    "nextCursor": "…"
  }
}
```

Use `POST /records/query` when a JSON filter is clearer or longer than a URL. Filter and sort only on public-queryable fields. A simple equality filter is a three-item array; compound filters use `and` or `or` arrays.

```json theme={null}
{
  "where": {
    "and": [
      ["status", "eq", "closed"],
      ["service_centre", "eq", "central"]
    ]
  },
  "sort": [{ "field": "created", "direction": "desc" }],
  "limit": 25,
  "search": "CSR-2026"
}
```

An invalid filter, cursor, limit, sort object, or protected/non-queryable field is a client correction task. Narrow the request and compare field keys with `GET /entities/:entityKey`; do not fall back to collecting an entire record set.

## Create records deliberately

Creation expects an object whose `data` value is another object. Server-side validation, entity permission checks, required-field rules, lifecycle rules, and duplicate-label checks still run.

```bash theme={null}
curl --request POST "$KAYANOS_API_BASE_URL/entities/service_requests/records" \
  --header "Authorization: Bearer $KAYANOS_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "data": {
      "request_id": "CSR-2026-00042",
      "service_centre": "central",
      "status": "received"
    }
  }'
```

A successful creation returns `201`, the readable record envelope, and an `ETag` when the record has a version. A create may still return `CREATED_RECORD_NOT_READABLE` if the owning token can create but cannot read the newly created record. Resolve that permission design before assuming the integration failed or retrying the creation.

## Update and delete with ETags

Read a record before changing or deleting it. The read response carries an `ETag` header derived from the record version. Send that exact value as `If-Match` on `PATCH` or `DELETE`.

```bash theme={null}
curl --include "$KAYANOS_API_BASE_URL/entities/service_requests/records/<record-id>" \
  --header "Authorization: Bearer $KAYANOS_API_TOKEN"
```

```bash theme={null}
curl --request PATCH "$KAYANOS_API_BASE_URL/entities/service_requests/records/<record-id>" \
  --header "Authorization: Bearer $KAYANOS_API_TOKEN" \
  --header 'If-Match: "17"' \
  --header "Content-Type: application/json" \
  --data '{ "data": { "status": "decision_review" } }'
```

`If-Match` is required. An absent header returns `428 PRECONDITION_REQUIRED`; an old version returns `412 PRECONDITION_FAILED`. The API accepts `If-Match: *`, but it intentionally skips the version comparison. Use it only when a documented integration owner has accepted overwriting concurrent changes; normal service workflows should use the exact `ETag` and re-read on `412`.

Deletion returns `{ "id", "entityKey", "deleted": true }` only after the same permission and version checks. It is not a bulk-delete endpoint. Build a review/retention process before giving a token the `delete` verb.

## Rate limits, headers, and retries

The deployed defaults are 60 authentication attempts per IP address per minute and 600 requests per token per minute. Deployments can configure those values, so read the response headers instead of hard-coding a limit:

| Header                  | Meaning                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `x-request-id`          | Correlation ID for the request.                                                      |
| `x-ratelimit-limit`     | Current window limit applied to the request.                                         |
| `x-ratelimit-remaining` | Requests remaining in the current window.                                            |
| `x-ratelimit-reset`     | Window reset time as Unix seconds.                                                   |
| `retry-after`           | Seconds to wait when the server returns `429`.                                       |
| `etag`                  | Current record version after a single-record read, create, or update when available. |

For `429`, wait at least `retry-after` seconds and apply bounded backoff. For `412`, re-read the record, compare the business change, and decide whether a new update is still correct. Never blindly retry a create or delete after a timeout: first determine whether the original request succeeded using a safe identifier or an owner-reviewed reconciliation process.

## Status and error reference

| Status | Typical code                                                                                                | Meaning and next action                                                        |
| ------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `400`  | `INVALID_BODY`, `INVALID_FILTERS`, `INVALID_LIMIT`, `INVALID_CURSOR`, `INVALID_SORT`, `FIELD_NOT_QUERYABLE` | Correct the client request or field selection.                                 |
| `401`  | `INVALID_TOKEN`, `TOKEN_REVOKED`, `TOKEN_EXPIRED`, `OWNER_INACTIVE`                                         | Rotate/re-authorize through the token owner; do not expose the failing secret. |
| `403`  | `PERMISSION_DENIED`, `CREATED_RECORD_NOT_READABLE`                                                          | Review owner verbs, scopes, field access, and lifecycle rules.                 |
| `404`  | `ENTITY_NOT_FOUND`, `RECORD_NOT_FOUND`                                                                      | Confirm the API-enabled entity key and a readable record ID.                   |
| `409`  | `DUPLICATE_LABEL`, `PARENT_REFERENCE`                                                                       | Reconcile the business conflict instead of retrying unchanged data.            |
| `412`  | `PRECONDITION_FAILED`                                                                                       | Re-read the record and resolve the concurrent update.                          |
| `428`  | `PRECONDITION_REQUIRED`                                                                                     | Supply `If-Match` for `PATCH` or `DELETE`.                                     |
| `429`  | `RATE_LIMITED`                                                                                              | Respect `retry-after` and lower request pressure.                              |
| `500`  | `SERVER_ERROR`                                                                                              | Retain the request ID and sanitized request context, then escalate.            |

## Worked integration: closed-request reporting

For a Service Centre reporting integration:

1. The organization enables only `service_requests` for the public API and assigns a reporting member the `list` verb in the appropriate Service Centre scope.
2. The integration calls `/me` at startup, records the token hint and request ID in a protected operational log, and stops if the organization or owner is unexpected.
3. It queries `status = closed` and the approved Service Centre with `limit: 25`, then follows `nextCursor` until the report window is complete.
4. It stores only the returned fields required for the report. It does not infer or fetch hidden citizen, financial, or staff fields.
5. If it must update a report-managed field, it first reads the record, supplies the returned `ETag`, and sends a narrow `{ "data": { … } }` patch. On `412`, it re-reads and asks the integration owner to resolve the changed state.

This gives the Directorate a traceable integration boundary without turning an API token into a blanket data export mechanism.

## Security and release checklist

* Use one named token per integration and per environment; do not share a person’s token between services.
* Give the owner the smallest set of entity verbs and scopes. Test denied access as deliberately as successful access.
* Set an expiry unless a documented service requirement and rotation process justify a non-expiring token.
* Store secrets only in an approved server-side secret store. Never ship one in a web or mobile client.
* Revoke a token immediately when its owner leaves, its purpose changes, or exposure is suspected; revocation stops it from working immediately.
* Log endpoint, status, request ID, token name/hint, and a redacted record reference—not the Authorization header, complete token, or sensitive payload.
* Test on an approved non-production record before enabling scheduled writes in production.

## Troubleshooting

| Symptom                                         | Check                                                            | Resolution                                                                                                      |
| ----------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `/me` returns `401`                             | Authorization scheme, token state, expiry, and owner login state | Create/rotate a token through the owner; never paste a real secret into a support request.                      |
| Entity is absent from `/entities`               | Public-API setting and at least one usable owner verb            | Enable the correct released entity and assign the minimum verb/scope needed.                                    |
| Field is absent or `FIELD_NOT_QUERYABLE` occurs | Entity metadata and field read-verb policy                       | Use an allowed field or change field access through approved governance; do not bypass it with a broader query. |
| `403` on a record that exists internally        | Owner role, scope, lifecycle rule, and record visibility         | Test with the token owner’s intended scope and correct the permission design.                                   |
| `412` on update/delete                          | Current `ETag` versus the saved header                           | Re-read, reconcile the business change, then send a fresh exact `If-Match`.                                     |
| Repeated `429`                                  | Rate-limit headers and concurrent workers                        | Reduce concurrency, honor `retry-after`, and use cursor pagination.                                             |

## Related guides

* [API tokens and public API administration](/administration/api-tokens-and-public-api)
* [Permissions and availability](/reference/permissions-and-availability)
* [Entities](/build/entities)
* [Troubleshooting](/reference/troubleshooting)
