Docs

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

ParameterTypeDefaultDescription
limitinteger50Maximum items per page (max: 100). Non-integers, zero, and negatives return 400. Values above 100 are clamped to 100.
startAfterstring—Cursor — pass nextCursor from the previous response. Must be a document id. Prefer this name.
cursorstring—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"
    }
  }
}
FieldDescription
limitNumber of items requested
hasMoreWhether more items exist after this page
nextCursorID 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).

ResourceDefault orderCommon filters
CustomersfirstName ascendingsearch (name, email, business)
Jobstimestamp descendingstatus, customerId, assignedTechnician
Servicestitle ascendingsearch
InventoryitemName ascendingsearch, categoryId, sku
QuotescreatedOn descendingcustomerId, ticketId, accepted
InvoicesinvoiceDate descendingcustomerId, ticketId, paid
Calendar eventsfrom ascendingfrom / to (range on start), assignedToId, ticketId
PaymentspaymentDate descendinginvoiceId
MessagescreatedAt ascendingticketId (required)
Recurring schedulesstartDate descendingstatus, 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_customers

Best 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.