Zeng Book
ProductIndustriesIntegrationsPricingResources
Sign inContact salesTry it free
Introduction
  • Overview
  • Quickstart
API reference
  • Authentication
  • Errors
  • Rate limits
  • Pagination
  • Interactive explorer
Resources
  • Organization
  • Leads
  • Clients
  • Projects
  • Quotations
  • Invoices
Integrations
  • Overview
  • Webhooks
  • Public portal
  • Zapier
  • Make / n8n recipes
  • Xero (coming soon)

API reference

Pagination

List endpoints return at most 50 records by default and 100 maximum. Larger result sets are walked with opaque cursors.

Query parameters

  • limit — integer, 1–100 (default 50). Out-of-range values clamp to the default.
  • cursor — opaque string returned as next_cursor from the previous page. Omit on the first call.

Response shape

json
{
  "data": [
    { "id": "cli_01HX...", "name": "Tan & Co", "createdAt": "2026-05-12T03:14:22.412Z" },
    { "id": "cli_01HW...", "name": "Lim Renovations", "createdAt": "2026-05-11T22:08:01.882Z" }
  ],
  "hasMore": true,
  "nextCursor": "eyJpZCI6ImNsaV8wMUhXLi4uIiwiY3JlYXRlZEF0IjoiMjAyNi0wNS0xMVQyMjowODowMS44ODJaIn0"
}
  • data — array of records, newest first by createdAt.
  • hasMore — boolean. When false, you've reached the end and nextCursor is null.
  • nextCursor — base64url-encoded JSON pointing to the next record. Treat it as opaque — its structure may change without notice.

Walking a full list

terminal
#!/bin/sh
KEY="zb_live_4xK2pQ7nR9sT1vW3yZ5aBd"
URL="https://www.zengbook.com/api/v1/clients?limit=100"

while [ -n "$URL" ]; do
  RES=$(curl -sS "$URL" -H "Authorization: Bearer $KEY")
  echo "$RES" | jq -c '.data[]'
  CURSOR=$(echo "$RES" | jq -r '.nextCursor // empty')
  if [ -z "$CURSOR" ]; then break; fi
  URL="https://www.zengbook.com/api/v1/clients?limit=100&cursor=$CURSOR"
done

In TypeScript

walk.ts
async function* walk<T>(path: string, key: string): AsyncGenerator<T> {
  let url: string | null = `https://www.zengbook.com/api/v1/${path}?limit=100`
  while (url) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${key}` },
    })
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
    const body = (await res.json()) as {
      data: T[]
      hasMore: boolean
      nextCursor: string | null
    }
    for (const row of body.data) yield row
    url = body.nextCursor
      ? `https://www.zengbook.com/api/v1/${path}?limit=100&cursor=${body.nextCursor}`
      : null
  }
}

Stability

New records during paging
The cursor is anchored on (createdAt, id) of the last row seen. Records inserted while you are paging will not appear in your walk — they sort newer than where you started. Re-run the walk (or page from a stored high-water mark) if you need a fresh snapshot.