Pagination
Cursor-based pagination across every list endpoint
Every list endpoint in the Servicebay API uses cursor-based pagination. You request a page size, receive that many items plus an opaque cursor, and pass the cursor back to fetch the next page.
Cursors are document ids. startAfter must match
[A-Za-z0-9_-]{1,128} or the request returns 400. A cursor returned
today still points at the same document tomorrow; items inserted above it
do not shift the page.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Maximum items per page (max: 100). Non-integers, zero, and negatives return 400. Values above 100 are clamped to 100. |
startAfter | string | — | Cursor — pass nextCursor from the previous response. Must be a document id. Prefer this name. |
cursor | string | — | Alias for startAfter on portal management lists (API keys, OAuth clients, grants). Accepted for one release; new clients should send startAfter. |
Invalid from / to ISO dates on calendar lists also return 400.
Response format
{
"success": true,
"data": {
"customers": [
/* ... */
],
"pagination": {
"limit": 50,
"hasMore": true,
"nextCursor": "abc123xyz"
}
}
}| Field | Description |
|---|---|
limit | Number of items requested |
hasMore | Whether more items exist after this page |
nextCursor | ID to pass as startAfter in the next request |
List order
List endpoints order by the field the Servicebay app uses. Equality filters
are applied in Firestore (they need a composite index — a missing index
returns 400, not a silent empty page).
| Resource | Default order | Common filters |
|---|---|---|
| Customers | firstName ascending | search (name, email, business) |
| Jobs | timestamp descending | status, customerId, assignedTechnician |
| Services | title ascending | search |
| Inventory | itemName ascending | search, categoryId, sku |
| Quotes | createdOn descending | customerId, ticketId, accepted |
| Invoices | invoiceDate descending | customerId, ticketId, paid |
| Calendar events | from ascending | from / to (range on start), assignedToId, ticketId |
| Payments | paymentDate descending | invoiceId |
| Messages | createdAt ascending | ticketId (required) |
| Recurring schedules | startDate descending | status, customerId, assetIds |
Customers created in the Servicebay app often have no createdAt. Ordering
the customer list by createdAt would hide them — that is why the API uses
firstName. Catalogue services often have a null index (that field is a
line-item ordinal), so the service list orders by title.
Fetching all pages
async function getAllCustomers(orgId, apiKey) {
const all = [];
let cursor = null;
do {
const url = new URL(
`https://developer.servicebay.io/api/v1/organisations/${orgId}/customers`
);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("startAfter", cursor);
const res = await fetch(url, { headers: { "X-API-Key": apiKey } });
const { data } = await res.json();
all.push(...data.customers);
cursor = data.pagination.hasMore ? data.pagination.nextCursor : null;
} while (cursor);
return all;
}import requests
def get_all_customers(org_id, api_key):
all_customers = []
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["startAfter"] = cursor
res = requests.get(
f"https://developer.servicebay.io/api/v1/organisations/{org_id}/customers",
headers={"X-API-Key": api_key},
params=params,
)
data = res.json()["data"]
all_customers.extend(data["customers"])
if not data["pagination"]["hasMore"]:
break
cursor = data["pagination"]["nextCursor"]
return all_customersBest practices
- Use page sizes of 25–100. Smaller pages waste round-trips, larger pages risk timeouts on busy organisations.
- Store the cursor if your UI lets users navigate back to a previous page.
- Show a loading state between pages so users know more data is coming.
- Cache aggressively for read-mostly data — pagination cursors are cheap to compare.