Skip to content
Last updated

Some collections — typically /v1/bookings and /v1/events — can return very large amounts of data. These endpoints page results so you can read the collection at your own pace.

Paginated endpoints are for ad-hoc use

Listing endpoints are designed for ad-hoc requests: rendering a list of events in a UI, generating an occasional report, or bootstrapping a local data store once. They are not designed as a continuous synchronization mechanism, and they do not return a consistent snapshot of the underlying collection.

If you need to keep an internal data store in sync with Understory, use webhooks instead. Use a listing endpoint to bootstrap your data once, and let webhooks drive ongoing updates from that point on.

Using cursor and limit

Paginated collections accept two query parameters:

  • cursor: an opaque string identifying where the next page begins. Omit it (or pass an empty string) to start at the beginning.
  • limit: the maximum page size. Endpoints document their default and maximum (most default to 100 and cap at 500).

Every paginated response is a JSON object with an items array and a next cursor:

$ curl https://api.understory.io/v1/bookings
{
  "next": "<cursor for the next page>",
  "items": [
    {...},
    {...}
  ]
}

If next is empty or absent, you have reached the end of the collection. Otherwise, repeat the request with cursor=<value of next> to fetch the next page.

Below is an example of paging through all bookings with a JavaScript while loop:

Paging through bookings
let next;
let bookings = [];

while (next) {
  const response = await fetch(
    `https://api.understory.io/v1/bookings?limit=100&cursor=${next}`,
    {
      headers: {
        authorization: `Bearer ${process.env.TOKEN}`,
        'user-agent': 'Understory API Demo',
      },
    }
  );

  if (response.status !== 200) {
    throw new Error(
      `Failed to fetch bookings: ${response.status} ${response.statusText}`
    );
  }

  bookings.push(response.items);

  next = response.next;
}

console.log(`Fetched ${bookings.length} bookings:`, bookings);

Responses are not a snapshot

A paginated response is a best-effort traversal, not a transactional snapshot. Neither a single page nor the sum of all pages represents a locked state of the collection at any point in time:

  • Two identical requests issued moments apart may return different totals.
  • A next cursor remains valid (it will not error), but it does not guarantee that subsequent pages are consistent with the page that produced it.
  • An item may appear on a later page, disappear before you reach it, or shift position if the field used to order results changes while you are paging.

Do not build logic that assumes the result set is stable across pages. Design for the case where the data shifts mid-traversal.

Cursors are opaque and short-lived

A cursor encodes the ordering and position of a specific paginated request at the moment it was issued. Treat it as a one-shot token:

  • Do not parse or modify cursors. Their format is internal and may change without notice.
  • Do not persist cursors across jobs, sessions, queues, or workflow state. A stored cursor replayed later will not give a stable continuation — the underlying data may have shifted, and the cursor's interpretation is tied to the request that produced it. Reusing it can return inconsistent, incomplete, or misleading results.
  • Use the cursor immediately, then discard it. A cursor is only meaningful for the next request in the same paging loop, issued promptly after receiving it. Always pass back the exact next value you received.

Why this happens

Most listing endpoints sort results by a domain field rather than by an immutable insertion timestamp. For example, GET /v1/events is ordered by session start time — the same field from and to filter on. A multi-session event is returned once, positioned by its earliest session in the requested window. This is intentional: we cannot both sort results by session start time and by a separate, stable insertion order at the same time, and sorting by start time is what makes the endpoint useful for the ad-hoc cases it is designed for.

The consequence is that if an event is created, updated, or has its start time changed inside your from/to window while you are paging, the ordering shifts and the result set you observe across pages can diverge from any single snapshot.

For continuous synchronization: use webhooks

If you are using a listing endpoint to keep a local data store in sync — periodically re-paging to find out what changed — switch to webhooks. That is the mechanism designed for the job:

  • New resource -> *.created webhook -> insert into your store.
  • Updated or rescheduled resource -> *.updated webhook -> update your store.
  • Deleted or cancelled resource -> *.deleted or *.cancelled webhook -> reflect the change.

Use the listing endpoint to bootstrap your store once, ad-hoc. Webhooks keep it in sync from there.