Skip to main content

Pagination & sorting

Every list endpoint is paginated and every one returns the same envelope — GET /v1/cooperatives, GET /v1/investors/{id}/share-purchases and GET /v1/investors/{id}/holdings. All use one-based page numbers, so page=1 is the first page. size defaults to 20 and is capped at 100; an oversized size is clamped rather than rejected.

Request parameters

ParameterTypeDefaultBoundsDescription
pageinteger1>= 1One-based page number. Out-of-range values clamp to the first page.
sizeinteger201..100Items per page. Out-of-range values clamp into the bounds.
sortstringcreated_at_descenum (below)Sort token.

Filter parameters (q, city, bafa_funded, dividend_type, min_share_price, max_share_price, affiliation) all combine via AND. Empty filters do not constrain the result set.

Sort tokens

TokenEffect
created_at_descNewest first. Default.
created_at_ascOldest first.
name_ascA → Z by name.
name_descZ → A by name.
share_price_ascCheapest share first.
share_price_descMost expensive share first.

An unknown sort token returns 400 VALIDATION_ERROR.

Response envelope

{
"items": [ /* CooperativeSummaryResponse[] */ ],
"page": 1,
"size": 20,
"total_items": 47,
"total_pages": 3,
"has_next": true,
"has_previous": false,
"sort": "created_at_desc"
}
FieldMeaning
itemsThe page's rows.
pageEcho of the requested page (one-based).
sizeEcho of the requested size (after clamping).
total_itemsTotal rows matching the filters, ignoring pagination.
total_pagesceil(total_items / size).
has_nextTrue when page < total_pages.
has_previousTrue when page > 1.
sortEcho of the sort token used.

Use has_next to terminate pagination loops; do not compute it client-side.

Stability across pages

Sort orderings are deterministic but not transactional — concurrent inserts or status changes can shift items across pages while you paginate. For most catalogue use-cases this is fine. If you need a perfect snapshot, fetch all pages once and dedupe on id client-side.

Caching

Both catalogue reads — GET /v1/cooperatives and GET /v1/cooperatives/{id} — return a strong ETag and a storable Cache-Control: private, max-age=300. Send the tag back as If-None-Match and an unchanged resource answers 304 Not Modified with no body:

curl -sS -D- -o/dev/null "https://api.valueverde.de/v1/cooperatives/$COOP_ID" \
-H "Authorization: Bearer $VV_ACCESS_TOKEN" \
-H 'If-None-Match: "3f9a1c08b2d54e7a91c6f0b3d8e2a745"'

The tag is derived from the rendered payload, not from a timestamp column. That matters: a cooperative's updated_at does not move when a dividend year is added, so a timestamp-based tag would have served you a stale 304. Comma-separated lists, the W/ prefix and * are all honoured.

The other /v1 endpoints carry no ETag. Investor profiles and share purchases change on your own writes, so you already know when they moved.

Image endpoints (cooperative hero, card, logo, project images) carry strong ETags and a 1-hour Cache-Control: max-age=3600.