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.
Before you send a request
An organization owner or an administrator with the relevant API-token permission should complete this checklist:- In the entity configuration, enable the entity for the public API. An entity that is not enabled returns
ENTITY_NOT_FOUNDthrough this interface, even if it exists internally. - Give the token owner only the required entity verbs:
list,create,update, and/ordelete. Scope assignment still applies to actual records. - 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.
- 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.
- Start with a read-only request against an approved non-production record with no personal data. Record the returned
x-request-idwith the integration test evidence.
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:data envelope; errors use an error envelope.
Endpoint map
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.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.
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.
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.
GET /entities/:entityKey; do not fall back to collecting an entire record set.
Create records deliberately
Creation expects an object whosedata value is another object. Server-side validation, entity permission checks, required-field rules, lifecycle rules, and duplicate-label checks still run.
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 anETag header derived from the record version. Send that exact value as If-Match on PATCH or DELETE.
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:
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
Worked integration: closed-request reporting
For a Service Centre reporting integration:- The organization enables only
service_requestsfor the public API and assigns a reporting member thelistverb in the appropriate Service Centre scope. - The integration calls
/meat startup, records the token hint and request ID in a protected operational log, and stops if the organization or owner is unexpected. - It queries
status = closedand the approved Service Centre withlimit: 25, then followsnextCursoruntil the report window is complete. - It stores only the returned fields required for the report. It does not infer or fetch hidden citizen, financial, or staff fields.
- If it must update a report-managed field, it first reads the record, supplies the returned
ETag, and sends a narrow{ "data": { … } }patch. On412, it re-reads and asks the integration owner to resolve the changed state.
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.

