Entity Feed Endpoint
This feature is not yet generally available. It will be available in our next major release.
The Entity Feed is the initial data load for a data lake or warehouse integration. It returns the current state of every record of a single entity type, in pages, stamped with the marker that tells you exactly where to begin Change Feed polling.
A new integration must acquire the full current dataset before incremental changes mean anything. /entityfeed is how you get it.
It is step two of the Data Feed lifecycle:
| Step | Endpoint | Purpose |
|---|---|---|
| 1. Schema Discovery | /schemas | Discover what can be loaded. |
| 2. Initial Data Load (this page) | /entityfeed | Bulk-load the current state of each entityDef. |
| 3. Change Feed | /changefeed | Poll for incremental changes after the initial load. |
New to the Data Feed? Start with Overview and Getting Started. It covers the Data Feed license, the API User group, obtaining a token, and the marker model — all of which this page assumes.
Use of this API requires a Data Feed license. See the Prerequisites section of Overview and Getting Started for more details.
Authenticate with an OAuth 2.0 bearer token —refer to Kahua API Authentication for provisioning credentials and requesting a token.
Beyond a valid token, the calling account must:
-
Belong to the API User group — otherwise 403.
-
Hold an assigned Data Feed license (kahua_DataFeed) — a purchased add-on, checked per caller, so every integration account needs its own seat. Otherwise 403.
POST https://{envName}service.kahua.com/v1/domains/{domainKey}/entityfeed
Authorization: Bearer <token>
Content-Type: application/json
Accept-Encoding: gzip, br (optional; honored if present)
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| envName | string | Name of the environment being used. It may be empty if using production or it could be devweekly during testing. |
| domainKey | string | The domain key. Value can be found from within Kahua in Domain Settings -> Authentication |
Query Parameters
None
This endpoint uses POST because it takes a structured JSON query in the request body. It makes no changes to your Kahua data.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| entityDefName | string | yes | — | Fully-qualified entity type to load, e.g. kahua_AEC_RFI.RFI. One entity type per request. |
| attributes | string[] | yes | — | The attribute projection — array of the field names to include in each entry's data map. There is no "select all". |
| pageSize | integer | no | 100 | Records per page. Clamped to [1, 1000]. |
| continuationToken | string | no | — | Opaque token from the previous response, to fetch the next page. Omit for the first page. |
| projectId | long | no | — | Restrict the load to a single project / partition. |
| asOfMarker | long | no | — | Pin the snapshot point. If omitted, the server captures the current marker on the first page and holds it for the rest of the load. Set this explicitly on every entity type after your first — refer to below. |
Why one entity type per request? It keeps paging deterministic, lets you load different entity types in parallel, and makes the attribute projection a simple list instead of a per-type map.
Getting the field names
The values you put in attributes are the name values from that entityDef's fields[] array in the /schemas response. A field name you make up will not error usefully — it simply will not appear in data.
First page
{
"entityDefName": "kahua_AEC_RFI.RFI",
"attributes": ["Number", "Subject", "Status", "Date", "DueDate", "Reason", "CostEffect"],
"pageSize": 500,
"projectId": 5001
}
Subsequent page
Repeat the request exactly, adding the continuationToken from the previous response.
{
"entityDefName": "kahua_AEC_RFI.RFI",
"attributes": ["Number", "Subject", "Status", "Date", "DueDate", "Reason", "CostEffect"],
"pageSize": 500,
"projectId": 5001,
"continuationToken": "eyJsYXN0SWQiOjg4MDEyLCJhc09mTWFya2VyIjo0NzAxfQ=="
}
Status 200 OK.
| Property | Type | Description |
|---|---|---|
| asOfMarker | long | The snapshot point for this load. This is the value you hand to Change Feed as lastMarker. Constant across every page of the load. |
| currentMarker | long | The newest marker in the domain at the moment of this response. Informational — it moves as other people work. |
| hasMore | boolean | true when more pages follow. This is the only correct way to decide whether to keep paging. |
| count | integer | Number of records in this page. |
| totalEstimate | long (key absent after the first page) | Total records matching your request. Refer to the note below. |
| continuationToken | string (key absent on the last page) | Opaque token to send on the next request. |
| entities | EntityFeedEntry[] | Array of records in this page, ordered by entityId ascending. |
| Property | Type | Description |
|---|---|---|
| entityDef | string | Fully-qualified entity type — the same for every entry in a response. |
| entityId | long | The record's ID. Use this as your warehouse's primary key. |
| partitionId | long | The project / partition the record belongs to. |
| data | object | Flat {fieldName: value} map containing exactly the attributes you requested. |
Read these before writing your parser.
Null fields are omitted, not emitted as null. Any envelope field whose value is null is left out of the JSON entirely. This applies to totalEstimate and continuationToken.
Do not detect the last page by testing continuationToken == null. On the last page the key is absent, and a strongly-typed client that requires it will break. Page on hasMore, and read continuationToken only when hasMore is true.
This does not extend to the data map. Inside data, a field that exists on the record but has no value is serialized as "Field": null. At the envelope level absence is the signal; inside data, null is a real value.
totalEstimate is computed on the first page only. The key is absent on every subsequent page — capture it from the first response rather than expecting it each time.
totalEstimate is exact, and it is not free. Despite the name it is an exact count, not a statistical estimate, and on a large entity type it makes that first page measurably slower. It exists to size a progress bar and never affects paging. If you don't need it, ignore it — there is currently no way to suppress the computation, so simply don't let your first-page latency budget depend on it.
Reference fields are returned as IDs. A reference to another record is returned as that record's numeric entityId (long); a collection of references is returned as an array of IDs (long[]). Referenced records are never expanded inline — load their entityDef separately if you need their details.
Initial response
{
"asOfMarker": 4701,
"currentMarker": 4720,
"hasMore": true,
"count": 500,
"totalEstimate": 2340,
"continuationToken": "eyJsYXN0SWQiOjg4NTEyLCJhc09mTWFya2VyIjo0NzAxfQ==",
"entities": [{
"entityDef": "kahua_AEC_RFI.RFI",
"entityId": 88001,
"partitionId": 5001,
"data": {
"Number": "0298",
"Subject": "Concrete mix design approval",
"Status": "Closed",
"Date": "2026-01-29T00:00:00Z",
"DueDate": "2026-02-15T00:00:00Z",
"Reason": "Clarification",
"CostEffect": "Decrease"
}
}
]
}
Last page
{
"asOfMarker": 4701,
"currentMarker": 4733,
"hasMore": false,
"count": 2,
"entities": [{
"entityDef": "kahua_AEC_RFI.RFI",
"entityId": 88512,
"partitionId": 5001,
"data": {
"Number": "0300",
"Subject": "Structural steel spec",
"Status": "Open",
"Date": "2026-02-02T00:00:00Z",
"DueDate": null,
"Reason": "Clarification",
"CostEffect": "None"
}
}
]
} }
Note that continuationToken and totalEstimate are absent — not present-and-null — while "DueDate": null inside data is a serialized value. asOfMarker is unchanged from the first page; currentMarker has moved because other users continued working.
Paging is cursor-based. Send the previous response's continuationToken back unchanged, along with the same entityDefName, projectId and attributes.
The token is opaque — do not parse, construct, or modify it. It carries your position in the result set and the pinned asOfMarker for the load.
A token is bound to the request that produced it. If you resume with a different entityDefName, a different projectId, or a different set of attributes, the call fails with 400 InvalidToken rather than silently paging the wrong data. Reordering or re-casing the attribute list is fine; genuinely changing any of the three is not. If you need to change the projection, restart the load from the beginning.
Pages are ordered by entityId ascending, and the cursor is stable — you will not skip or duplicate records, and you may pause between pages.
Every page of one load is served against the same snapshot point, so the load is internally consistent even though other users keep working while it runs.
-
Records created after asOfMarker are excluded.
-
Records edited after asOfMarker return their current state, not their state at the snapshot. Any edit you "miss" this way arrives again as an Edit on Change Feed, so nothing is lost.
-
Records deleted after asOfMarker are excluded rather than returned. Change Feed would only have delivered a Delete you would have applied anyway.
-
Soft-deleted records never appear at all.
A record created in the same instant as the snapshot boundary may appear in the load and again as an Add on Change Feed. This is rare, bounded, and deliberate — the boundary is inclusive because dropping a legitimately-in-snapshot record would be worse than delivering one twice. Change Feed is an upsert stream, so re-applying the same current state is harmless. Make your warehouse writes idempotent and this costs you nothing.
This is the part integrations most often get wrong, and getting it wrong causes silent, permanent data loss.
Do this: load your first entity type, take its asOfMarker, and pass that value explicitly as asOfMarker on every subsequent entity type. All your loads then share one snapshot point, and you start Change Feed from that single marker.
{
"entityDefName": "kahua_AEC_DailyReport.DailyReport",
"attributes": ["Number", "Date", "Shift", "StatusWorkflow"],
"pageSize": 1000,
"asOfMarker": 4701
}
All loads must complete within the ~30-day marker retention window, or the pinned marker returns 410 SessionExpired.
If you did not pin a shared marker and each entity type captured its own asOfMarker, start Change Feed from the MIN of them — never the MAX.
A type pinned below the MAX loses every change between its own marker and the MAX: those changes aren't in its snapshot, and Change Feed skips them because its cursor is exclusive (markerId > lastMarker). Using the MIN only re-delivers rows you already have, which is harmless on an idempotent upsert stream.
-
Excluded entity types. Kahua infrastructure record types and internal apps are not available through the feed. Requesting one returns 400 InvalidRequest, and the error message names why.
-
Child records are loaded as their own entityDefName request. Children are never nested inside their parent.
-
Deleted records are never returned.
-
No per-record permission filtering. This is a bulk system-to-system channel: within your domain it returns all non-excluded records of the requested type, without applying the per-record visibility rules a human user would see in the Kahua UI. Apply your own access control downstream.
-
Only your requested attributes appear in data.
-
Rate limit: by default 60 requests per minute, per calling account, per domain — and this window is shared with /changefeed. Loading several entity types in parallel draws on the same budget.
-
Compression: send Accept-Encoding: gzip, br. On large pages this typically reduces payload size 5–10×.
-
Page size: larger pages mean fewer round trips against your rate limit. 1000 is the maximum and a good default for a bulk load.
-
Parallelism: different entity types can be loaded concurrently, subject to the shared rate limit. Pages within one entity type are inherently sequential — each needs the previous page's token.
Response Body
| Property | Type | Description |
|---|---|---|
| errorCode | string | Code that represents the error condition |
| message | string | Additional information about the error |
Error codes 410 and 429 include additional properties described in the error codes table.
Codes
| Status | error | Notes |
|---|---|---|
| 400 | InvalidRequest | Missing entityDefName or attributes, pageSize out of range, or an excluded entity type was requested. |
| 400 | InvalidToken | The continuation token is malformed, or does not match this request's entityDefName / projectId / attribute set. Restart the load. |
| 403 | Forbidden | No access to the domain, not a member of the API User group, or no Data Feed license (kahua_DataFeed). |
| 404 | EntityDefNotFound | The entityDefName does not exist in this domain. Re-check it against /schemas. |
| 410 | SessionExpired | The pinned asOfMarker has aged out of the ~30-day retention window — whether it came from a continuation token or from an explicit asOfMarker. Body includes currentMarker. Restart the load without a token. |
| 429 | RateLimited | Rate limit exceeded. Sleep retryAfterSeconds included in the body (or honor Retry-After), then retry the same request with the same continuation token — you lose no progress. |
curl -X POST 'https://service.kahua.com/v1/domains/MagsevenBuilders/entityfeed' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-H 'Accept-Encoding: gzip, br' \
--data '{
"entityDefName": "kahua_AEC_RFI.RFI",
"attributes": ["Number", "Subject", "Status", "Date", "DueDate"],
"pageSize": 1000,
"projectId": 5001
}'
1. Load the FIRST entity type
POST /v1/domains/MagsevenBuilders/entityfeed
{ "entityDefName": "kahua_AEC_RFI.RFI", "attributes": [...], "pageSize": 1000 }
→ Page until hasMore == false, sending back continuationToken each time
→ Record asOfMarker from the response. Call it M.
2. Load every REMAINING entity type, pinned to M
POST { "entityDefName": "...", "attributes": [...], "asOfMarker": M }
→ Every type now shares one snapshot point.
→ All loads must finish inside the approximate 30-day retention window.
3. Transition to Change Feed, starting at M
POST /v1/domains/MagsevenBuilders/changefeed
{ "lastMarker": M, "pageSize": 1000, "entityDefNames": [...], "attributes": {...} }
(If you did NOT pin M, use MIN(asOfMarker) across all loads — never MAX.)
4. On 410 SessionExpired
Restart the load from the beginning: omit continuationToken and asOfMarker.
5. On 429 RateLimited
Sleep retryAfterSeconds, then retry with the same continuationToken.
-
Data Feed — Overview and Getting Started
-
Schema Discovery — /schemas
-
Change Feed — /changefeed