Base API pagination
Base API endpoints that can return large record collections use a shared page-number pagination contract. This bounds response size and serialization work while preserving each endpoint's authorization, company and cooling-unit scope, filters, and search behavior.
!!! warning "Breaking response change"
Paginated endpoints return an object containing `results`; they no longer
return a top-level JSON array or an endpoint-specific pagination envelope.
Mobile and web clients must be updated before deploying the corresponding
backend release.
Request parameters
| Parameter | Default | Limits | Description |
|---|---|---|---|
page |
1 |
Positive base-10 integer | One-based page number. |
page_size |
25 |
Positive base-10 integer, maximum 200 |
Number of records requested. |
Each parameter may appear at most once. Values such as 0, negative numbers,
decimals, non-numeric text, repeated parameters, and a page_size greater than
200 return 400 Bad Request with an error keyed by the invalid field.
Response contract
{
"count": 250,
"page": 1,
"page_size": 25,
"total_pages": 10,
"next": true,
"previous": false,
"results": []
}
count is the number of records remaining after authorization, tenant or
cooling-unit scope, filtering, search, and duplicate removal. Those operations
and deterministic ordering are applied in the database before the page is
sliced and serialized.
A valid page beyond the available range returns 200 OK with an empty
results array. An empty collection uses count: 0, total_pages: 0, and
sets both navigation flags to false.
Ordering
Pagination requires deterministic ordering so adjacent pages do not duplicate or omit records under normal use:
- Historical and event records sort by descending event date or timestamp,
followed by descending
id. - Named reference records normally sort by ascending name, followed by
ascending
id. - Other collections use their documented endpoint ordering with
idas a tie-breaker, or descendingidas the fallback.
Offset-based pages can still shift when records are inserted concurrently. Clients that need a frozen export should use a stable date range or another endpoint-specific snapshot filter where available.
Affected endpoint groups
The shared contract applies to high-volume or query-intensive lists in these Base API areas:
| Area | Paginated collections |
|---|---|
| Users | Users, active-check-in users, archived users, companies, cooling users, operators, service providers, invitations, cooling-user surveys, and notifications |
| Storage | Cooling units, locations, crates, checked-in produce, next checkouts, and pricing-plan resources |
| Operations | Check-ins, checkouts, movements, usage, revenue events, storage payments, refunds, and market surveys |
| Marketplace | Buyer listings and orders; seller listings, coupons, and orders; company orders and delivery contacts |
| Audit | Audit logs |
| Prediction | Data-table result collections |
Small, naturally bounded configuration or aggregate payloads remain unpaginated. Examples include crop and crop-type taxonomies, cooling-unit crop configuration, current cooling-unit specifications, map topology, current cart resources, and fixed-window prediction graphs.
Marketplace order ordering
GET /marketplace/buyer/orders/ and GET /marketplace/seller/orders/ accept
an ordering parameter of created_at or -created_at (default
-created_at), with id/-id as a deterministic tiebreak. Any other value
returns a field-specific 400 response.
Marketplace compatibility alias
GET /marketplace/buyer/available-listings/ temporarily accepts the legacy
items_per_page parameter. It has the same maximum of 200 and is deprecated.
If both parameters are provided, page_size takes precedence. The response
always uses the shared envelope above.
Analytics API exception
The token-authenticated endpoints under /api/v1/analytics/* and
/api/v1/sensor-data retain their public data and meta.pagination
contract, with a default page size of 100 and maximum of 500. Do not apply
the Base API response parser to those endpoints. See
Analytics API examples.
Movement search and ordering
GET /operation/movements/, GET /operation/movements/usage/, and
GET /operation/movements/revenue/ share the same search and ordering
query parameters, applied before pagination:
| Parameter | Format | Behavior |
|---|---|---|
search |
Free text | Case-insensitive match against movement code, cooling-user name, company name, and crop name, across both check-in and checkout data. |
ordering |
One of crop_type, movement_date, movement_date_reverse, check_in_first, check_out_first, cooling_user_name |
Selects a deterministic sort. Defaults to movement_date_reverse. An unsupported value returns a field-specific 400 response. |
Movement usage endpoint
GET /operation/movements/usage/ uses the shared pagination response and the
search/ordering parameters above, plus the existing cooling_units scope
and these optional filters:
| Parameter | Format | Behavior |
|---|---|---|
start_date |
YYYY-MM-DD |
Includes movements from the start of this date. |
end_date |
YYYY-MM-DD |
Includes movements through the end of this date. |
The date range is applied in SQL before counting and slicing. Invalid dates, or
a start_date after end_date, return a field-specific 400 response.
The endpoint remains check-in driven: it selects movements whose checked-in produce used one of the authorized cooling units. A matching movement can also contain checkout data. Existing serialized item fields were preserved.
Movement revenue endpoint
GET /operation/movements/revenue/ derives one revenue event per matching
payment and paginates the resulting event list, not the underlying movements.
It accepts the existing cooling_units and payment_methods scope plus the
shared search/ordering parameters above.
Ordering is applied differently here because events do not map one-to-one onto movements:
- If
orderingis provided, events are sorted by their movement's position in the ordered movement queryset, with descending eventidas a tiebreaker. - If
orderingis omitted, events sort by descendingoccurred_at(payment date), not movement date.
Client migration
Replace code that treats an affected response as an array:
// Before
const records = await response.json();
// After
const page = await response.json();
const records = page.results;
Fetch subsequent pages while next is true:
async function fetchAllPages(url, options) {
const records = [];
let pageNumber = 1;
while (true) {
const requestUrl = new URL(url);
requestUrl.searchParams.set("page", String(pageNumber));
requestUrl.searchParams.set("page_size", "200");
const response = await fetch(requestUrl, options);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const page = await response.json();
records.push(...page.results);
if (!page.next) return records;
pageNumber += 1;
}
}
Prefer rendering one page at a time instead of immediately reconstructing the old unbounded response in memory.
Performance characteristics
Filtering, authorization scope, ordering, counting, and slicing happen before response serialization. Related records needed by collection serializers are loaded in page-level batches to avoid issuing the same lookup once per result.
Pagination bounds the number of top-level records, but it does not necessarily
bound nested arrays inside each record. For example,
/storage/v1/cooling-units/{id}/produces/ can return a page of produce records
where each produce still contains multiple crates. Consumers should avoid
requesting the maximum page size when they do not need it and should render
pages incrementally.
Migration checklist
When updating an API consumer:
- Read records from
resultsand retain the pagination metadata needed by the interface. - Remove assumptions that affected responses are top-level arrays or use the
legacy marketplace
nodes/paginationfields. - Preserve existing filters and authorization context when requesting the next page.
- Test empty, first, intermediate, final, and out-of-range pages.