Every list endpoint is paginated the same way.
| Parameter | Default | Max | Description |
|---|---|---|---|
page | 1 | — | Page number, 1-based. |
per_page | 25 | 100 | Items per page. |
curl "https://<host>/api/public/v1/users?page=2&per_page=50" \
-H "X-Api-Key: $DUCKDAY_API_KEY"
{
"data": [ ... ],
"pagination": {
"page": 2,
"per_page": 50,
"total": 137,
"total_pages": 3
}
}
A page past the last one answers with an empty data array and the real
pagination.total. A page, per_page, updated_since or date filter that is
not a valid value — per_page=500, updated_since=yesterday,
date_from=2026-02-30 — answers 422 validation_error, with details naming
the parameter.
Lists are ordered by id ascending. Ids are monotonically increasing, so
new records always append at the end: iterating page by page never skips
or duplicates rows created while you paginate.
For periodic synchronization, avoid re-reading everything: filter with
updated_since (ISO 8601) and keep a high-water mark on your side.
curl "https://<host>/api/public/v1/users?updated_since=2026-07-01T00:00:00Z" \
-H "X-Api-Key: $DUCKDAY_API_KEY"
Store the timestamp you started the sync at, and pass it as updated_since
on the next run. Every timestamp the API returns is UTC with a Z suffix, to
the millisecond. URL-encode the offset if you send one (+02:00 is %2B02:00):
an unencoded + arrives as a space and the value is rejected.
Day filters (date_from/date_to, due_from/due_to) are inclusive at both
ends and name whole days: date_to=2026-07-31 includes a record at 23:59:59.
A to-do due at a specific time counts on the company's calendar day — a
deadline at 00:30 in Rome on the 20th is due on the 20th, although its UTC
timestamp reads the 19th. An all-day due date (due_is_all_day: true) and an
expense date are the day that was entered, in every timezone. Overlapping windows are safe — process records
idempotently by id.