API Reference
Pagination
Cursor model, page size, 24-hour TTL, and the full meta.pagination shape for every list endpoint in the Kubeadapt /v1 API.
List endpoints in /v1 use opaque cursor pagination. Each response carries a cursor in meta.pagination.next_cursor; pass it back as ?cursor=<token> on the next request. The walk terminates when the cursor is empty. There is no ?page=2, ?skip=100, or ?sort= parameter — pages are sorted by cost descending on cost endpoints and by time descending on time-ordered endpoints. Modifying the cursor returns 400 INVALID_CURSOR; a cursor older than 24 hours returns 410. Cost Explorer is the one exception — it uses offset pagination (page/per_page).
Default and maximum page size
The ?limit= query parameter controls page size on every list endpoint:
| Bound | Value |
|---|---|
Default (when ?limit= is absent) | 100 |
| Minimum | 1 |
| Maximum | 500 |
Out-of-range values (?limit=0, ?limit=501, ?limit=-5, ?limit=abc) return a 400 BAD_REQUEST with details[0].field = "limit". The maximum is 500.
Cursors expire after 24 hours
Cursors older than 24 hours return 410 CURSOR_EXPIRED with details[0].reason = "expired". Restart the walk from page 1 (no cursor) to recover.
1{
2 "data": null,
3 "meta": {
4 "request_id": "req_01J5K3V0Q7Y4XR8A2B3C5D7E9F",
5 "applied_at": "2026-05-22T11:14:02Z"
6 },
7 "error": {
8 "code": "CURSOR_EXPIRED",
9 "message": "cursor older than 24h, restart pagination",
10 "details": [
11 { "reason": "expired" }
12 ]
13 }
14}Cursors are bound to the query that produced them
A cursor is bound to the query parameters that produced it (filters, cost_mode, limit). Changing any of these between pages returns CURSOR_EXPIRED with details[0].reason = "query_changed".
1{
2 "data": null,
3 "meta": {
4 "request_id": "req_01J5K3V0Q7Y4XR8A2B3C5D7E9G",
5 "applied_at": "2026-05-21T10:31:15Z"
6 },
7 "error": {
8 "code": "CURSOR_EXPIRED",
9 "message": "cursor query parameters changed between pages",
10 "details": [
11 { "reason": "query_changed" }
12 ]
13 }
14}Which parameters affect cursor validity
| Parameter | Can change between pages? |
|---|---|
cursor | Yes — it is the page selector. |
include_total | Yes — toggling the count does not invalidate the cursor. |
limit | No. |
Any filter (provider, region, status, kind, …) | No. |
cost_mode | No (on endpoints that accept it). |
Every parameter other than cursor and include_total must be byte-identical across all pages of a single walk.
The meta.pagination shape
Every paginated list response carries this block under meta:
1{
2 "meta": {
3 "request_id": "req_01J5K3V0Q7Y4XR8A2B3C5D7E9F",
4 "applied_at": "2026-05-21T10:30:00Z",
5 "pagination": {
6 "next_cursor": "eyJ2IjoxLCJhZnRlcl9pZCI6ImMxYTJiM2M0LWQ1ZTYtNzg5MC1hYmNkLWVmMTIzNDU2Nzg5MCIsImFmdGVyX3ZhbHVlIjoiMTQ3LjI0MDAiLCJxdWVyeV9oYXNoIjoiYTNiNGM1ZDZlN2Y4ZzlhMGIxYzJkM2U0ZjVnNiIsImlzc3VlZF9hdCI6MTcxNjI4NTAwMH0",
7 "has_more": true,
8 "limit": 100,
9 "total_count": 386
10 }
11 }
12}| Field | Type | Notes |
|---|---|---|
next_cursor | string | Opaque token. Empty string when no more pages exist (the stop signal for a walk). |
has_more | boolean | Same stop signal in cleaner form. true while pages remain, false on the final page. |
limit | integer | Echo of the resolved page size (100 by default). |
total_count | integer (optional) | Total rows matching the filter, ignoring limit and cursor. Present only when the request supplied ?include_total=true. |
next_cursor == "" and has_more == false are always populated together; either is sufficient to terminate the loop.
include_total is opt-in
meta.pagination.total_count is the total row count matching the filter (ignoring cursor and limit). Default off; opt in with ?include_total=true when you need a total count for progress indicators.
curl -H "Authorization: Bearer ka_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
"https://public-api.kubeadapt.io/v1/workloads?limit=50&include_total=true"Sort policy
List endpoints sort by their natural key in descending order. There is no ?sort= or ?order= parameter.
The natural sort key per list family:
| Endpoint family | Sort key | Direction |
|---|---|---|
/v1/clusters | cost.current_run_rate_hourly | DESC |
/v1/clusters/{id}/namespaces, /v1/namespaces | cost.current_run_rate_hourly | DESC |
/v1/clusters/{id}/workloads, /v1/workloads | cost.current_run_rate_hourly | DESC |
/v1/workloads/{uid}/pods | cost.current_run_rate_hourly | DESC |
/v1/clusters/{id}/nodes | cost.current_run_rate_hourly | DESC |
/v1/recommendations | savings.estimated_monthly | DESC |
/v1/teams, /v1/departments | cost.current_run_rate_hourly | DESC |
/v1/teams/{id}/assignments | metadata.created_at | DESC |
Endpoints that do NOT paginate
Three families return their full result set inline with no meta.pagination block:
GET /v1/node-groupsandGET /v1/clusters/{cluster_id}/node-groups. Node groups are bounded in cardinality (a customer with 50 clusters has a few hundred node groups, not millions). The response is the complete list; nonext_cursor, nohas_more.- Single-resource GETs.
GET /v1/clusters/{cluster_id},GET /v1/workloads/{workload_uid},GET /v1/teams/{team_id}, and the rest of the by-ID endpoints return one resource.datais a single object, not an array;meta.paginationis absent. GET /v1/organizationandGET /v1/organization/dashboard. These are tenant-level snapshots, not collections. One object in, one object out.
meta.pagination is absent whenever data is a single object, and also absent on node-groups responses where data is an array. Branch on the endpoint family, not on field presence.
A full pagination walk
Three back-to-back requests pulling pages 1, 2, 3 of /v1/workloads. The response is abbreviated to one workload per page so the pagination shape stays visible; in real responses, data is an array of limit (here 100) items.
Page 1: no cursor
curl -H "Authorization: Bearer ka_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
"https://public-api.kubeadapt.io/v1/workloads?limit=100"1{
2 "data": [
3 {
4 "id": "b9f1a342-7c5e-4d8a-9e2b-1234567890ab",
5 "kind": "Workload",
6 "metadata": {
7 "name": "api-gateway",
8 "workload_kind": "Deployment",
9 "namespace": "checkout",
10 "cluster": { "id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890", "name": "prod-eu-west-1" }
11 },
12 "cost": {
13 "current_run_rate_hourly": { "amount": "12.4800", "currency": "USD" },
14 "cost_mode": "fully_loaded",
15 "last_updated_at": "2026-05-21T10:29:00Z"
16 }
17 }
18 ],
19 "meta": {
20 "request_id": "req_01J5K3V0Q7Y4XR8A2B3C5D7E9F",
21 "applied_at": "2026-05-21T10:30:00Z",
22 "pagination": {
23 "next_cursor": "eyJ2IjoxLCJhZnRlcl9pZCI6ImI5ZjFhMzQyLTdjNWUtNGQ4YS05ZTJiLTEyMzQ1Njc4OTBhYiIsImFmdGVyX3ZhbHVlIjoiMTIuNDgwMCIsInF1ZXJ5X2hhc2giOiJhM2I0YzVkNmU3ZjhnOWEwYjFjMmQzZTRmNWc2IiwiaXNzdWVkX2F0IjoxNzE2Mjg1MDAwfQ",
24 "has_more": true,
25 "limit": 100
26 },
27 "cost_mode": "fully_loaded"
28 }
29}Page 2: pass the cursor back
curl -H "Authorization: Bearer ka_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
"https://public-api.kubeadapt.io/v1/workloads?limit=100&cursor=eyJ2IjoxLCJhZnRlcl9pZCI6ImI5ZjFhMzQyLTdjNWUtNGQ4YS05ZTJiLTEyMzQ1Njc4OTBhYiIsImFmdGVyX3ZhbHVlIjoiMTIuNDgwMCIsInF1ZXJ5X2hhc2giOiJhM2I0YzVkNmU3ZjhnOWEwYjFjMmQzZTRmNWc2IiwiaXNzdWVkX2F0IjoxNzE2Mjg1MDAwfQ"1{
2 "data": [
3 {
4 "id": "a4d20f51-9b3c-4a17-8e6d-2345678901bc",
5 "kind": "Workload",
6 "metadata": {
7 "name": "cart-service",
8 "workload_kind": "Deployment",
9 "namespace": "checkout",
10 "cluster": { "id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890", "name": "prod-eu-west-1" }
11 },
12 "cost": {
13 "current_run_rate_hourly": { "amount": "4.7100", "currency": "USD" },
14 "cost_mode": "fully_loaded",
15 "last_updated_at": "2026-05-21T10:29:00Z"
16 }
17 }
18 ],
19 "meta": {
20 "request_id": "req_01J5K3V0Q7Y4XR8A2B3C5D7E9G",
21 "applied_at": "2026-05-21T10:30:08Z",
22 "pagination": {
23 "next_cursor": "eyJ2IjoxLCJhZnRlcl9pZCI6ImE0ZDIwZjUxLTliM2MtNGExNy04ZTZkLTIzNDU2Nzg5MDFiYyIsImFmdGVyX3ZhbHVlIjoiNC43MTAwIiwicXVlcnlfaGFzaCI6ImEzYjRjNWQ2ZTdmOGc5YTBiMWMyZDNlNGY1ZzYiLCJpc3N1ZWRfYXQiOjE3MTYyODUwMDh9",
24 "has_more": true,
25 "limit": 100
26 },
27 "cost_mode": "fully_loaded"
28 }
29}Page 3: the final page
curl -H "Authorization: Bearer ka_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
"https://public-api.kubeadapt.io/v1/workloads?limit=100&cursor=eyJ2IjoxLCJhZnRlcl9pZCI6ImE0ZDIwZjUxLTliM2MtNGExNy04ZTZkLTIzNDU2Nzg5MDFiYyIsImFmdGVyX3ZhbHVlIjoiNC43MTAwIiwicXVlcnlfaGFzaCI6ImEzYjRjNWQ2ZTdmOGc5YTBiMWMyZDNlNGY1ZzYiLCJpc3N1ZWRfYXQiOjE3MTYyODUwMDh9"1{
2 "data": [
3 {
4 "id": "f8b6c7e3-1a92-4f5b-bc8e-3456789012cd",
5 "kind": "Workload",
6 "metadata": {
7 "name": "db-migrator",
8 "workload_kind": "Job",
9 "namespace": "payments",
10 "cluster": { "id": "d8e9f0a1-b2c3-4567-89ab-cdef01234567", "name": "staging-us-east-1" }
11 },
12 "cost": {
13 "current_run_rate_hourly": { "amount": "0.0900", "currency": "USD" },
14 "cost_mode": "fully_loaded",
15 "last_updated_at": "2026-05-21T10:29:00Z"
16 }
17 }
18 ],
19 "meta": {
20 "request_id": "req_01J5K3V0Q7Y4XR8A2B3C5D7E9H",
21 "applied_at": "2026-05-21T10:30:16Z",
22 "pagination": {
23 "next_cursor": "",
24 "has_more": false,
25 "limit": 100
26 },
27 "cost_mode": "fully_loaded"
28 }
29}next_cursor is the empty string and has_more is false. Stop the loop.
A complete walk in pseudocode
1url = "https://public-api.kubeadapt.io/v1/workloads"
2params = {"limit": 500}
3all_rows = []
4
5while True:
6 response = http_get(url, params=params, headers=auth_headers).json()
7 all_rows.extend(response["data"])
8 page = response["meta"]["pagination"]
9 if not page["has_more"]:
10 break
11 params["cursor"] = page["next_cursor"]Two constraints on the loop:
- Cap iterations with a max-iterations guard and add a Retry-After-aware sleep on
429. - Keep every query parameter constant across iterations except
cursorandinclude_total. Mutatingcost_mode,limit, or any filter starts a different walk and invalidates the cursor.
See also
- API Overview, the envelope shape and the resource family index.
- Error Handling, the full error code catalog and the retry guide.
INVALID_CURSORandCURSOR_EXPIREDlive here. - Cost Modes, which endpoints accept
?cost_mode=and which reject it. On accepting endpoints,cost_modemust not change between pages of a single walk. - Workloads, the canonical large list endpoint, the one most users walk first.
- Recommendations, sorted by
savings.estimated_monthlydescending instead of cost.