Help center

Pagination

GET /sops and GET /assignments return pages of up to 100 items. Pass next_cursor back as cursor, with the same filters, until has_more is false.

Business plan Updated
On this page
  1. Which endpoints are paginated
  2. Response shape
  3. Getting the next page
  4. Cursor rules
  5. Items that change while you page

Which endpoints are paginated

Endpoint Paginated Order Default limit Max limit
GET /sops Yes, cursor updated_at ascending (oldest change first), then id 50 100
GET /assignments Yes, cursor created_at descending (newest first), then id 50 100
GET /search No, best matches only Rank, best first 20 50
GET /libraries No, everything in one response See the endpoint
GET /boards No, everything in one response position
GET /people No, everything in one response Name

Response shape

A paginated response has three top-level fields:

JSON
{
  "data": [
    { "id": "5b7e9d1a-2c4f-4e6b-8a1d-3c5e7f9a0b03", "title": "New hire onboarding (example)" }
  ],
  "has_more": true,
  "next_cursor": "eyJ0cyI6IjIwMjYtMDktMThUMDk6NDE6MDcuMDA0NTEyWiIsImlkIjoiNWI3ZTlkMWEtMmM0Zi00ZTZiLThhMWQtM2M1ZTdmOWEwYjAzIn0"
}
Field Type Meaning
data array This page’s items. Can be [].
has_more boolean true when at least one more item exists after this page.
next_cursor string or null Pass as cursor to get the next page. null when has_more is false.

Getting the next page

  1. Make the first request without cursor.
  2. If has_more is true, repeat the request with cursor set to next_cursor and the same filters (library_id, status and so on). The cursor records a position in the ordering, not your filters, so changing filters mid-way gives you a page of a different list.
  3. Stop when has_more is false.
Shell
curl "https://app.sopcompass.com/api/v1/sops?limit=100&cursor=eyJ0cyI6IjIwMjYtMDktMThUMDk6NDE6MDcuMDA0NTEyWiIsImlkIjoiNWI3ZTlkMWEtMmM0Zi00ZTZiLThhMWQtM2M1ZTdmOWEwYjAzIn0" \
  -H "Authorization: Bearer $SOPCOMPASS_API_KEY"
JavaScript
async function* allPages(path, params = {}) {
  let cursor = null;
  do {
    const qs = new URLSearchParams({ ...params, limit: "100", ...(cursor ? { cursor } : {}) });
    const res = await fetch(`https://app.sopcompass.com/api/v1${path}?${qs}`, {
      headers: { Authorization: `Bearer ${process.env.SOPCOMPASS_API_KEY}` },
    });
    if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error.code}`);
    const page = await res.json();
    yield* page.data;
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
}

for await (const sop of allPages("/sops", { include_archived: "true" })) {
  console.log(sop.updated_at, sop.title);
}
Python
import os
import requests

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SOPCOMPASS_API_KEY']}"


def all_pages(path, **params):
    cursor = None
    while True:
        query = {**params, "limit": 100}
        if cursor:
            query["cursor"] = cursor
        res = session.get(f"https://app.sopcompass.com/api/v1{path}", params=query, timeout=30)
        res.raise_for_status()
        page = res.json()
        yield from page["data"]
        if not page["has_more"]:
            break
        cursor = page["next_cursor"]


for assignment in all_pages("/assignments", status="overdue"):
    print(assignment["due_on"], assignment["assignee"]["email"], assignment["sop"]["title"])

Cursor rules

  • Cursors are opaque. Don’t decode, build or edit them; their format can change within v1. A cursor that can’t be read answers 422 invalid_cursor.
  • Cursors don’t expire. A cursor marks a position (the last item’s sort key), not a snapshot, so you can resume later.
  • Ties are handled. Positions keep microsecond precision and break ties on id, so items written in the same millisecond are neither skipped nor repeated.
  • limit must be a whole number from 1 to 100. Anything else answers 422 invalid_parameter.

Items that change while you page

Because pages are positions in a live list:

  • GET /sops is ordered by updated_at. An SOP edited while you page moves to the end of the list, so you see it again on a later page with its new content. De-duplicate by id and keep the copy with the latest updated_at. This is what makes updated_since syncs complete: nothing that changes is ever missed.
  • GET /assignments is newest first. Assignments created after your first request sort before your position, so they don’t appear in the pages that follow. Start a new pass from the first page to pick them up.