---
title: "Pagination, filtering, rate limits, and versioning"
section: "Developer Platform"
canonical_url: "https://support.buildwithhq.com/api/api-operating-contracts.html"
reviewed_at: "2026-09-14"
authority: Official
---

# Pagination, filtering, rate limits, and versioning

Implement list traversal, server-side filters, 429 recovery, and contract pinning from the exact OpenAPI operation.

## In brief

- **Module lists:** pageNumber + hasMore
- **Organization lists:** afterAppAccountId cursor
- **Rate limits:** 429 + required Retry-After

BuildWithHQ uses one public v1 API with operation-specific list contracts. The OpenAPI operation defines its parameters, defaults, bounds, response shape, credential class, and scopes. A client should not impose one pagination or filter shape on routes that publish different contracts.

[OpenAPI and SDKs](versioning-and-openapi.html) [Limits and idempotency](limits-and-quotas.html) [Download the executable example](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.zip)

## Use the declared pagination family

| List family | Request | Continue with | Stop when |
| --- | --- | --- | --- |
| Most first-class modules | `pageNumber`, `pageSize`, and declared filters | Increment `pageNumber` | `hasMore` is false |
| Organization discovery | `afterAppAccountId`, `pageSize`, optional `search` | Return `nextAfterAppAccountId` as the next cursor | The next cursor is absent |
| Tenant-user directory | No paging or cursor parameters | No follow-up request | The complete `users` array is returned |
| Bounded history/projection routes | The operation's documented `limit` or other bound | Only as that response declares | The operation-specific condition is reached |

Module list envelopes can report `pageNumber`, `pageSize`, `totalRecords`, `totalPages`, `totalIsExact`, `hasMore`, and `items`. Drive traversal with `hasMore`. Treat a total as exact only when `totalIsExact` is true.

Contacts and Global Search default to `includeTotal=false` and return a `browseLimit` of 5,000. Request an uncapped authorized total through the dedicated count endpoint, without blocking row rendering. See [progressive loading and exact counts](progressive-loading-and-counts.html)  for routes, scopes, cache boundaries and the executable SDK example.

**Sync/export migration:** a module-list `hasMore=false` at this cap does not prove a complete directory. For all authorized Contacts records use `POST /v1/apps/{saasAppId}/objects/contacts/records/search` with `page`, `pageSize` (1–100) and a fixed sort, following that operation's continuation. The linked guide explains its generic record shape, native-field hydration and lack of snapshot guarantees.

The tenant-user list returns the whole authorized organization directory in one response. Its exact `totalCount` equals the length of `users`; each row includes DataRole and location counts and assignments. Zero selected locations means all locations, not no access. Do not send legacy page or cursor parameters. No per-organization user ceiling is enforced today, so response size grows with users and assignments. The expected case is a few hundred users; large-directory limits or streaming remain pre-launch scale work. A dependency failure returns 503, not an empty list.

## Book an eligible tenant user

Call `GET /v1/apps/{saasAppId}/modules/reservations/assignees` with `reservationProductId` and `startUtc` together, plus an optional `locationId`. Choose an item from `items` and send its `optionId` as `assignedUserId` to Reservation purchase or reschedule; `lookupId` identifies the lookup contract and is not a user ID. The server rechecks eligibility and capacity on write. On `409 reservations_conflict`, refresh the Reservation and assignee choices. There is no generic `/modules/reservations/lookups` route; use `/products` and `/assignees`.

## Filter before data reaches the browser

Send only filters declared for the operation, such as `search`, `status`, `locationId`, or a module-specific state. Omit an unused filter. The server applies verified app, account, user, DataRole, Location, and permission scope before the requested filter. IDs and cursors select targets inside that boundary; they do not grant access.

## GoClaw authority errors in the tenant runtime

For the signed-in tenant route `PUT /api/goclaw/agents/{goClawAgentId}/authority`, handle `400 goclaw_authority_invalid_request` by correcting the request, `403 goclaw_authority_forbidden` as a permission denial, and `404 goclaw_authority_not_found` by refreshing the authorized agent list. The route is not an app-scoped Developer API operation. Kit 1.20.70 includes the exact mapping in `catalog/tenant-runtime-error-codes.json`; framework-level model validation can also return 400 with a different response shape.

## Handle tenant page and workflow status failures

The authenticated tenant page-save route `POST /api/runtime/pages/{pageKey}/revisions/save` requires the verified account owner and the builder's tenant-edit policy. A missing page returns `404 page_revision_not_found`; a protected page returns `403 page_revision_not_editable`. Require `report.Saved=true` even when HTTP is 200, and preserve the E020 edit-lease, E021 ID-loss acknowledgement and E022 stale-version reports.

For `PUT /api/inbox/items/{inboxItemId}/human-state`, keep the current `expectedActivityVersion`. A pending workflow approval cannot be resolved, closed, snoozed or moved to WaitingCustomer: the server returns `400 inbox_request_invalid` without changing state. Complete the authorized workflow decision first; older stranded items can be reopened. Stale revisions return `409 inbox_stale_context`. IDs and refreshes never grant edit or approval authority. Kit 1.20.70 records these codes in `catalog/tenant-runtime-error-codes.json`.

## Honor the rate-limit contract

Every published `429 Too Many Requests` response declares a required `Retry-After` header containing whole seconds with a minimum of one. Wait that long, then retry within a bounded attempt count. Use capped exponential backoff with jitter for transient 502, 503, and 504 responses.

```
const waitSeconds = Number(response.headers.get("Retry-After"));
if (response.status === 429 && Number.isInteger(waitSeconds) && waitSeconds >= 1) {
  await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
}
```

GET and HEAD requests can be retried safely. Retry a write only when its operation supports an idempotency key or explicitly defines repeat-safe behavior. Reuse the same key and payload. Preserve the API error `correlationId` when the bounded attempts end.

## Pin and update the contract

Generate against the OpenAPI file in the immutable Developer Kit. For an app with marketplace services, call `GET /v1/apps/{saasAppId}/openapi.json` and pin `X-BuildWithHQ-Contract-SHA256` or the matching document fingerprint. Regenerate when it changes. Clients must ignore additive response properties they do not recognize; breaking changes require an explicit API version and migration guidance.

## Run the offline example

The Developer Kit includes `examples/api-client-policy.mjs` with a bounded request loop, numbered-page iterator, and organization cursor iterator. Its test uses mocked HTTP responses and needs no credential:

```
node --test examples/api-client-policy.test.mjs
```

Note

Page-size bounds and allowed filters belong to each OpenAPI operation. Never copy one route's maximum, default, status values, or cursor semantics into another client method.

---

Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/api/api-operating-contracts.html).
