Pagination
List endpoints return at most one page of results. Every list response wraps records in data and describes the page in meta.
curl "https://api.wefunder.com/investments?per_page=25" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [ ... ],
"meta": {
"has_more": true,
"page_count": 25,
"next_cursor": "12345"
}
}
Fetching the next page
Pass meta.next_cursor back unchanged as the cursor query parameter:
curl "https://api.wefunder.com/investments?per_page=25&cursor=12345" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
When meta.has_more is false you have the last page. What next_cursor holds on that page depends on the kind of endpoint:
- Listing endpoints (most of the API, including
GET /exploreandGET /campaigns) returnnullor omit it. You are done. - Sync endpoints (
GET /investmentsand the Investment Changes endpoint) always return anext_cursor, including on the last page. That final cursor is your sync position: store it and pass it ascursoron your next run to receive only what changed since. Sync Investments to Your CRM walks through this.
A loop that sets its cursor to null when has_more is false is correct for listing endpoints but silently discards your position on GET /investments. The next run re-bootstraps from scratch or misses investments. Loop on has_more, not on the cursor, and persist the last next_cursor you received.
Parameters
| Param | Default | Max | Description |
|---|---|---|---|
per_page | 25 | 100 | Records per page. Values above 100 are capped to 100; zero or negative values fall back to the default. |
cursor | – | – | The previous page's meta.next_cursor, passed back verbatim. |
A few endpoints have their own page size, documented on each endpoint.
Treat the cursor as opaque
The cursor's shape depends on the endpoint. Depending on how the underlying list is ordered it may be a record id, an offset, or an ISO 8601 timestamp. Do not parse it, do not construct one, and do not assume two endpoints share a format. Pass back exactly what meta.next_cursor gave you.
Iterating every page
The loop ends on has_more, not on the cursor, and returns the last next_cursor it saw. For a listing endpoint that value is null; for a sync endpoint it is the position to persist.
async function fetchAll(path, onRecord) {
let cursor = null;
let body;
do {
const params = { per_page: 100 };
if (cursor) params.cursor = cursor;
({ data: body } = await api.get(path, { params }));
for (const record of body.data) await onRecord(record);
cursor = body.meta.next_cursor ?? null;
} while (body.meta.has_more);
return cursor;
}
// Listing endpoint: the return value is null once every page is read.
await fetchAll("/campaigns", (campaign) => console.log(campaign.id));
// Sync endpoint: the return value is the sync position for the next run.
const syncCursor = await fetchAll("/investments", (investment) => upsert(investment));
await store.saveCursor(syncCursor);
Best practices
- Use the maximum page size for syncs and smaller pages for interactive UIs.
- Stop on
has_more: false, not on an empty page. - Process records as you go rather than buffering the whole set.
- Store cursors only from sync endpoints. A listing cursor describes a position in a live list; start a fresh walk each time. A sync endpoint's final cursor is the one value you must persist between runs.