Important Changes to `/api/v2/assets/{uid_asset}/data` Result Limits

Dear community,

In our continuous efforts to deliver a smooth and reliable service, we’re making an important update to the /api/v2/assets/{uid_asset}/data endpoint that will help preserve performance, stability and scalability of our platform.

:warning: Please note:
This change does not affect the synchronous export endpoints (i.e.
/api/v2/assets/{uid_asset}/export-settings/{uid_export}/data.xlsx|csv).
These export routes will continue to operate exactly as before.

Effective in the first January release (during the week of January 12, 2026), the default limit of results returned per request will be changed:

  • The maximum number of results per page will now be 1,000 (instead of 30,000).

  • The default number of results per page (if you do not explicitly specify limit=) will now be 100 (previously 30,000).

Why this change? As our user base expands, maintaining a fast and reliable experience for everyone requires continuous optimization. Restricting the amount of data retrieved per call allows us to balance server load and sustain the same high-quality performance as the platform scales.

What to do?

  • If you are currently requesting very large pages of data (e.g., 30,000 records at once), please update your application logic to page through results in increments of 1,000 or fewer.

  • Use the next/previous links in the response to iterate through the data set safely.

  • If you specify limit= explicitly, you may request up to 1,000 records per page.

Example response after request:

{
  "count": 1000,
  "next": "https://kf.kobotoolbox.org/api/v2/assets/aGAcvhamUDuDTtbkyeikbD/data.json?limit=100&start=100",
  "previous": null,
  "results": [ … ]
}

If you have any questions or experience any issues, please reach out via our usual support channels.

Warm regards,

The KoboToolbox team

Code Examples

Python

import requests
API_URL = "https://kf.kobotoolbox.org/api/v2/assets/YOUR_ASSET_ID/data.json"
TOKEN = "YOUR_TOKEN_HERE"
headers = {"Authorization": f"Token {TOKEN}"}
start = 0
limit = 100
while True:
    params = {"limit": limit, "start": start}
    resp = requests.get(API_URL, headers=headers, params=params)
    resp.raise_for_status()
    data = resp.json()
    print(f"Fetched {len(data['results'])} records (start={start})")
    if not data["next"]:
        break
    start += limit

R

library(httr)
library(jsonlite)
api_url <- "https://kf.kobotoolbox.org/api/v2/assets/YOUR_ASSET_ID/data.json"
token <- "YOUR_TOKEN_HERE"
start <- 0
limit <- 100
repeat {
  resp <- GET(api_url,
              add_headers(Authorization = paste("Token", token)),
              query = list(limit = limit, start = start))
  stop_for_status(resp)
  data <- fromJSON(content(resp, as = "text", encoding = "UTF-8"))
  cat("Fetched", length(data$results), "records (start =", start, ")\n")
  if (is.null(data$next)) break
  start <- start + limit
}

Node.js (JavaScript)

const fetch = require("node-fetch");
const API_URL = "https://kf.kobotoolbox.org/api/v2/assets/YOUR_ASSET_ID/data.json";
const TOKEN = "YOUR_TOKEN_HERE";
async function fetchAll(limit = 1000) {
  let start = 0;
  while (true) {
    const url = new URL(API_URL);
    url.searchParams.append("limit", limit);
    url.searchParams.append("start", start);
    const resp = await fetch(url.toString(), {
      headers: { "Authorization": `Token ${TOKEN}` },
    });
    const data = await resp.json();
    console.log(`Fetched ${data.results.length} records (start=${start})`);
    if (!data.next) break;
    start += limit;
  }
}
fetchAll().catch(console.error);

curl

TOKEN="YOUR_TOKEN_HERE"
ASSET_ID="YOUR_ASSET_ID"
BASE_URL="https://kf.kobotoolbox.org"
# First page
curl -H "Authorization: Token $TOKEN" \
     "$BASE_URL/api/v2/assets/$ASSET_ID/data.json?limit=1000&start=0"
# Next page
curl -H "Authorization: Token $TOKEN" \
     "$BASE_URL/api/v2/assets/$ASSET_ID/data.json?limit=1000&start=1000"

:light_bulb: Tip: You can also fetch data incrementally by using a query parameter with the last known _id.

For example:

?query={"_id":{"$gte": <last_pk> }}

This will return only submissions with `_id` greater or equal to <last_pk>.

You can combine it with pagination parameters like limit and start.

Python

import requests, json
API_URL = "https://kf.kobotoolbox.org/api/v2/assets/YOUR_ASSET_ID/data.json"
TOKEN = "YOUR_TOKEN_HERE"
headers = {"Authorization": f"Token {TOKEN}"}
params = {
    "limit": 1000,
    "query": json.dumps({"_id": {"$gte": 52149}})  # fetch from last known _id, here 52149
}
resp = requests.get(API_URL, headers=headers, params=params)
resp.raise_for_status()
data = resp.json()
print(f"Fetched {len(data['results'])} records starting from _id >= 52149")
8 Likes

@ambassadors for your updates!

3 Likes

Hi @Kal_Lam

Can you please confirm that the limit will also affect API requests in Power Query, and this paging logic would also be possible in Power Query?

1 Like

mi enlace es por medio de excel, como puedo indicar la paginacion alli?

How will this work for data imports into Excel? This does not seem to be covered by the examples; and the current guidance - Using the API for synchronous exports — KoboToolbox documentation - does not (yet?) cover this change. For example, in Excel the data source (in Power Query Editor) is: =Excel.Workbook(Web.Contents(“https://kf.kobotoolbox.org/api/v2/assets/TOKEN/export-settings/#######################/data.xlsx”), null, true) - how would that change to accommodate downloading pages?

Thanks, Oscar

2 Likes

Hello,

To remove any ambiguity, the following clarification has been added to the original post:

Only the former endpoint will be subject to the new limit.
The synchronous export endpoint remains fully intact and unchanged.

1 Like

@Isslam please see @Olivier’s response:

1 Like

EDIT: We are planning to include the change in the first January release, during the week of January 5, 2026.

1 Like