Kahua Data Feed — Overview and Getting Started
This feature is not yet generally available. It will be available in our next major release.
The Kahua Data Feed is a set of three read-only APIs that let an external system — a data warehouse, data lake, BI platform, or reporting database — build and maintain its own synchronized copy of your Kahua data.
You use all three, in order:
| Step | Endpoint | Purpose |
|---|---|---|
| 1. Discover | /schemas | Find out what entity types exist in your domain and what fields each one has. |
| 2. Load | /entityfeed | Bulk-load the current state of each entity type you care about. |
| 3. Poll | /changefeed | Receive incremental Add / Edit / Delete changes from that point forward, forever. |
You run steps 1 and 2 once when you set up the integration. You run step 3 on a schedule (for example every five minutes) from then on.
Read this page first. The three endpoint pages assume you have completed the prerequisites below. The single most common support case on this API is a caller who has valid credentials but has not been granted the Data Feed entitlement, and receives a 401 or 403 that looks like a password problem.
You need all four of the following before your first call will succeed.
1. A Data Feed license (kahua_DataFeed)
The Data Feed is a purchased add-on entitlement, not part of the base platform. Please contact your account representative if you are interested in purchasing this add-on. Without it, every Data Feed call returns 403 Forbidden with the message "The caller has not purchased Data Feed access."
The license is checked per calling account, not per domain. That means each integration user calling the API needs its own assigned Data Feed seat. Assigning the license to one account does not enable another.
2. An integration user in the API User group
The calling account must be a member of the API User group. This is the standard integration-identity group in Kahua, and is a prerequisite for the Kahua API generally — refer to Authentication for how an administrator grants it.
Note the failure modes differ by endpoint, which is a common source of confusion:
- On /entityfeed and /changefeed, a non-member receives 403 Forbidden.
- On /schemas, a non-member receives 401 Unauthorized — the group is checked earlier in the request pipeline for that endpoint, so it surfaces as an authentication failure rather than an authorization one. A 401 from /schemas does not necessarily mean your token is bad.
3. OAuth 2.0 credentials and a bearer token
The Data Feed uses OAuth 2.0. One set of credentials works across all three endpoints, and every request carries the resulting token in an Authorization: Bearer <token> header.
Follow Kahua API Authentication to provision a client ID and client Secret, request a token, and use it in your requests. That page is the authoritative source for authentication, including the token endpoint for your environment and token expiry behavior.
4. Your host, domain key, and project ID
-
Host — https://{envName}service.kahua.com. This is environment-specific.
- Domain key — the identifier for your Kahua domain, used in the /entityfeed and /changefeed URLs. Replace any spaces in the domain name with an underscore; for example, a domain named Magseven Builders has a domain key of Magseven_Builders. The value can also be found in the Authentication section of the Domain Settings app.
- Project ID — a numeric ID, used in the /schemas URL and as an optional filter on the two feeds. In Kahua, a project and a partition are the same thing; the API uses both words, and responses return it as partitionId.
These terms appear throughout the three endpoint pages.
| Term | Meaning |
|---|---|
| Domain | Your Kahua tenant. All Data Feed calls operate within a single domain. |
| Domain key | The domain's URL-safe identifier, with spaces replaced by underscores. |
| App | An installed Kahua application, e.g. kahua_AEC_RFI. |
| entityDef | An entity definition — a record type, e.g. an RFI or a Daily Report. Always written fully-qualified as AppName.SchemaName, e.g. kahua_AEC_RFI.RFI. |
| Entity | One record of an entityDef — a single RFI. |
| Attribute / field | A single value on an entity, e.g. Subject. The /schemas response calls these fields; the feeds call them attributes. They are the same thing. |
| Partition / project | The container a record belongs to. Returned as partitionId. |
| Marker | The central concept of the Data Feed. Every change to any entity in your domain is stamped with a marker — an ever-increasing numeric ID. Markers are ordered domain-wide and never reused, so a marker ID is a bookmark: "I have processed everything up to here." You store the last marker you processed and hand it back on your next poll. |
| Attribute projection | The explicit list of attributes you want returned. There is no "select all" anywhere in the Data Feed — if you don't ask for a field, you don't get it. |
Markers are retained for approximately 30 days. Older markers are archived off and deleted.
This has three practical consequences:
- Your initial load must finish within the retention window. If you pin a snapshot marker and your load is still running 30 or more days later, it will fail with 410.
- You cannot pause polling for more than approximately 30 days. If your stored marker ages out, your next /changefeed call returns 410 MarkerExpired.
- Recovering from an expired marker means re-running /entityfeed, not restarting ChangeFeed from zero. Refer to below — this is important and easy to get wrong.
lastMarker: 0 is not a full re-sync. Starting ChangeFeed from 0 replays only the markers still retained — roughly the last 30 days of activity. An entity that has not been touched in 31 days has no surviving marker and will never appear. If you need a complete, correct dataset again, re-run the /entityfeed initial load. Use lastMarker: 0 only when you already hold a complete dataset and simply need to re-apply recent changes.
/entityfeed and /changefeed share a single rate window: by default 60 requests per minute, per calling account, per domain. Because the window is shared, loading several entity types in parallel consumes the same budget as polling — plan your concurrency accordingly.
Exceeding the limit returns 429 with a retryAfterSeconds value in the body and a Retry-After header. Honor it and resume from exactly where you left off; a 429 costs you nothing but time.
/schemas is not subject to this window. It is cacheable instead — refer to that page's ETag support.
The two endpoint families use different error envelope shapes. Do not write a single error parser that assumes one shape.
/entityfeed and /changefeed:
{
"error": "RateLimited",
"message": "Rate limit exceeded. Retry after the indicated interval.",
"retryAfterSeconds": 24
}
currentMarker is also present on 410 responses.
{
"errorCode": "404",
"message": "..."
}
| Status | Meaning | What to do |
|---|---|---|
| 400 | Your request is invalid — a bad field, a bad page size, an excluded entityDef, or a continuation token that doesn't match this request. | Fix the request. Do not retry unchanged. |
| 401 | Not authenticated — or, on /schemas only, authenticated but not in the API User group. | Check group membership before assuming the token is wrong. |
| 403 | Authenticated, but no domain access, not in API User group, or no Data Feed license. | Check the entitlement first. |
| 404 | The entityDef you named does not exist, is excluded, or belongs to an app you cannot access. | Re-run /schemas to find what is actually available to you. |
| 410 | Your marker has aged out of retention. | Re-run the /entityfeed initial load. |
| 429 | Rate limited. | Sleep retryAfterSeconds, then retry the same request. |
All Data Feed responses are serialized with null values omitted. A field whose value is null is left out of the JSON entirely rather than written as "field": null.
Write your consumer to treat a missing key and a null value as equivalent. Never require a key to be present, and never test for a literal null to detect end-of-data — test the boolean flag (hasMore) instead.
The one place a literal null is meaningful is inside the data map of a feed entry: an attribute that exists on the record but has no value is a real, serialized "Attr": null.
The feeds and /schemas share one exclusion list, so what you can discover is exactly what you can load. Two categories of entityDef are automatically hidden:
- Kahua infrastructure record types — internal plumbing that carries no business data.
- Record types belonging to internal Kahua apps.
Certain field types are also never returned, because they carry no consumer-usable value in a feed: encrypted fields, lock state, file and image attachments, GUIDs, and raw XML.
/schemas is the authoritative list of what is available to you. Rather than working from the categories above, call it and build your integration from what it returns — anything absent from that response cannot be loaded from the feeds either.
Escape hatch: naming an entityDef explicitly in /changefeed's entityDefNames bypasses exclusion for that entityDef. Use this only if you have a specific, confirmed need for an internal record type.
This is the correct sequence. Read the marker handling carefully — it is where integrations most often introduce silent data loss.
1. DISCOVER — once, at setup
GET /api/v1/projects/{projectId}/apps/schemas
→ Review schemas[] and each entityDef's fields[]
→ Choose the entityDefs and the exact field names your warehouse needs
→ Save the ETag so you can cheaply detect schema drift later
2. INITIAL LOAD — the FIRST entity type
POST /v1/domains/{domainKey}/entityfeed
{ "entityDefName": "kahua_AEC_RFI.RFI", "attributes": [...], "pageSize": 1000 }
→ Page until hasMore == false, following continuationToken
→ Record the asOfMarker from the response. Call it M.
3. INITIAL LOAD — every REMAINING entity type, pinned to M
POST { "entityDefName": "...", "attributes": [...], "asOfMarker": M }
→ Passing asOfMarker explicitly puts every entity type on ONE snapshot point.
→ All loads must finish inside the ~30-day retention window.
4. TRANSITION TO POLLING — start from M
POST /v1/domains/{domainKey}/changefeed
{ "lastMarker": M, "pageSize": 1000, "entityDefNames": [...], "attributes": {...} }
5. POLL — forever, on your interval
→ Page until hasMore == false AND marker == currentMarker: you are caught up
→ Persist the returned marker after each page
→ Sleep your interval, repeat from the stored marker
If you did not pin a shared asOfMarker in step 3 and each entity type captured its own, then start ChangeFeed from the MIN of the markers — never the MAX.
A type pinned below the MAX loses every change in the gap between its own marker and the MAX: those changes are not in its snapshot, and ChangeFeed skips them because its cursor is exclusive (markerId > lastMarker). That is permanent, silent data loss. Using the MIN only re-delivers rows you have already seen, which is harmless — ChangeFeed is an upsert stream and re-applying a row is idempotent.
Detecting schema drift
Re-run /schemas periodically with If-None-Match set to your saved ETag. A 304 Not Modified means nothing changed and costs almost nothing. A 200 means an entityDef or field set has changed — for example after an app upgrade — and your attribute projections may need updating.
- Explicit projections only. No endpoint supports "select all". This keeps payloads small and makes your integration's data dependencies visible.
- Ids, not nested objects. A reference to another record is returned as that record's numeric entity ID. Referenced records are never expanded inline. Fetch them separately by loading their own entityDef.
- No per-record permission filtering. The Data Feed is a system-to-system bulk channel. Within your domain, it returns all non-excluded records of the requested type — it does not apply the per-record visibility rules a human user would see in the Kahua UI. Treat the resulting warehouse as containing full-domain data, and apply your own access control downstream.
- Deleted records are excluded from /entityfeed and arrive as Delete actions on /changefeed.
- Schema Discovery — /schemas
- Entity Feed — /entityfeed
- Change Feed — /changefeed