Pagination
Keyset cursor pagination for all list endpoints.
List endpoints return a uniform envelope: the rows under data, and pagination state
under page.
{ "data": [ /* … */ ], "page": { "nextCursor": "…", "hasMore": true } }Query parameters
| Param | Meaning |
|---|---|
limit | Page size, default 50, max 500. |
cursor | Opaque cursor from a previous page.nextCursor. Omit for the first page. |
sort | A whitelisted sort key for the resource (see below). |
order | asc or desc. |
q | Free-text filter over resource-specific columns. |
Paging through results
Follow page.nextCursor until page.hasMore is false (at which point nextCursor
is null):
let cursor = undefined;
const all = [];
do {
const url = new URL("https://app.steerd.io/api/public/v1/contacts");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: "Bearer strd_YOUR_KEY" } });
const { data, page } = await res.json();
all.push(...data);
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);The cursor is keyset-based, so it is stable under inserts and deletes; you will
not skip or repeat rows because the underlying data changed between pages. A cursor is
tied to the sort/order it was issued for: passing it with a different sort or
order returns 400 bad_request.
Pass only a cursor Steerd issued. A cursor value that does not decode is also
400 bad_request. It is not read as "start from the beginning", so a cursor your own
plumbing truncated fails loudly instead of quietly restarting the loop at page one. To ask
for the first page, omit cursor (or send it empty).
Sort keys per resource
Defaults are shown in bold.
| Resource | Sort keys |
|---|---|
| organizations | name, created_at |
| contacts | name (last, first), email, created_at |
| employees | name (last, first), title, created_at |
| cvs | name, updated_at, created_at |
| projects | updated_at, created_at, title, status, archived_at |
| activities | activity_date, created_at |
| tasks | created_at |
| assignments | created_at |
| time_entries | entry_date |
| trips | created_at, approved_at |
Default order is asc for the name-defaulted resources and desc for the others.
trips takes no q; it filters with status instead, as an enum, so a misspelled value is a
400 rather than a silently empty page.
Projections
Responses are public projections: internal-only fields are never exposed. Every resource
omits team_id, and several omit more than that.
The API Reference is the authoritative field list, generated from the OpenAPI document, so a resource's schema there is exactly what you get. What follows is the reasoning behind the omissions, not a second copy of the list:
- Attribution and tenancy internals.
user_idon activities and employees,assigned_user_idon tasks,created_by_other/edited_by_otheron time entries. These identify the acting Steerd user, which the public surface does not model. - UI state.
board_rankandattentionon projects; thevisibilityflag on files. - Nested internal objects. A time entry omits the embedded
employee; read it through/employees/{id}withemployee_id. - Fields whose public surface does not exist yet. An organization omits
requires_xrechnungandrecipient_kind, and each of its addresses omitselectronic_address,electronic_address_schemeandestablishment_country. These pair with invoicing, and there is no public invoice resource for them to pair with; every public field is a compatibility commitment, so they stay internal until there is. - Reshaped rather than removed. A trip replaces
sealed_totalswithtotalsplustotals_sealed, and the detail omitscomputation(an untyped blob).