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
| Parameter | Type | Default | Bounds | Description |
|---|---|---|---|---|
page | integer | 1 | >= 1 | One-based page number. Out-of-range values clamp to the first page. |
size | integer | 20 | 1..100 | Items per page. Out-of-range values clamp into the bounds. |
sort | string | created_at_desc | enum (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
| Token | Effect |
|---|---|
created_at_desc | Newest first. Default. |
created_at_asc | Oldest first. |
name_asc | A → Z by name. |
name_desc | Z → A by name. |
share_price_asc | Cheapest share first. |
share_price_desc | Most 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"
}
| Field | Meaning |
|---|---|
items | The page's rows. |
page | Echo of the requested page (one-based). |
size | Echo of the requested size (after clamping). |
total_items | Total rows matching the filters, ignoring pagination. |
total_pages | ceil(total_items / size). |
has_next | True when page < total_pages. |
has_previous | True when page > 1. |
sort | Echo 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.