API
Pagination
How list endpoints paginate, when to use limit/offset versus the export stream, and how to walk large workspaces without hitting limits.
Every list endpoint returns paginated results. ProductOS uses limit + offset, with a separate export endpoint that returns every row of an entity in one response.
Default: limit + offset
GET /api/v1/feedback?limit=50&offset=100| Parameter | Default | Max | Meaning |
|---|---|---|---|
limit | 50 | 200 | Rows per page |
offset | 0 | – | Rows to skip |
sort | per-entity default | – | Sort field; see the entity reference |
order | per-entity default | – | asc | desc |
Response envelope:
{
"data": [ /* up to limit rows */ ],
"pagination": {
"limit": 50,
"offset": 100,
"total": 423
},
"links": {
"self": "https://productos.ca/api/v1/feedback?limit=50&offset=100",
"next": "https://productos.ca/api/v1/feedback?limit=50&offset=150",
"prev": "https://productos.ca/api/v1/feedback?limit=50&offset=50"
}
}pagination.total is the total matching rows for your filter, useful for "Showing 51–100 of 423" UIs.
Trashed rows and drafts are left out of list responses unless you pass include_deleted=true or include_drafts=true.
When limit/offset gets slow
OFFSET is O(rows skipped) at the database level. On workspaces with >10k rows of a single entity, paginating past offset ~10k starts to feel slow (>500ms). Two options:
Filter your way down
The easier fix: combine with filters. GET /api/v1/feedback?status=new&offset=5000 is much faster than GET /api/v1/feedback?offset=5000 because the index narrows the scan.
Use the export endpoint
For full-dataset exports (analytics, backups, migrations), use the export endpoint:
GET /api/v1/{entity}/export?format=jsonIt returns every row of the entity in one response, oldest first, including drafts and trashed rows (check deleted_at). format=json returns { "data": [...], "count": n }; format=csv (the default) returns a file download:
curl 'https://productos.ca/api/v1/feedback/export?format=csv' \
-H "Authorization: Bearer $TOKEN" \
-o feedback.csvExports have their own rate bucket, 6 per minute per user, separate from list reads.
Sort stability
The default sort field varies per entity (created_at for most; votes for feedback, date for releases). Rows are ordered by the one field you pick, with no automatic tiebreaker, so when many rows share a value (say votes = 5) the same offset=50 can land on different rows between calls and you'd see duplicates or gaps. For a stable walk, sort by a unique field such as id_number or created_at.
Filtering
Every entity lists its filterable fields on its reference page (for example feedback). Repeat a filter to OR multiple values (a comma-separated value is matched literally, not split):
GET /api/v1/feedback?status=new&status=under-reviewFilters AND across different fields, OR within the same field. There's no support for negation or range filters via the URL syntax, use the export endpoint + downstream filtering for those cases.
Search
?q=substring runs a case-insensitive substring match across the entity's searchable fields. It's a LIKE %term% query, not full-text, fine for short queries on workspaces up to ~100k rows. Semantic search across all entities (POST /api/ai/search) is available to the in-app session only, not to API tokens; the MCP server offers search_workspace for connected clients.
Junction expansion
For entities with junction tables, request inline expansion to avoid N+1:
GET /api/v1/feedback?expand=themes,features&limit=50Name any of the entity's junctions, comma-separated; unknown names are ignored. Expansion counts as a slightly larger query but one request against the rate limit, not one per row.
Next
- Rate limits: what to watch when walking large datasets.
- Feedback entity reference: what fields are filterable and sortable per entity.