Pagination
Last updated 28 October 2026 · API version v1
Every list in this API is paged the same way: forward, a page at a time, with a cursor you pass back unchanged.
Cursors, never offsets
The issue list sorts on a composite key, and an offset drifts under concurrent writes — page 2 would skip rows a scan inserted while you were reading page 1.
A product whose subject is a defensible record cannot show a page that silently repeats or skips rows. So there is no page parameter and no offset parameter — which has a consequence worth knowing before you design around it: a cursor cannot answer give me page 26. A list moves forward one page at a time, and the total is a number rather than a page count.
What a cursor is, and is not
The cursor is opaque. Do not parse it, do not construct one, and do not store it as a bookmark — it encodes a sort position, not a record.
A cursor the API cannot read — a truncated one, or anything that is not a cursor — is answered with the first page. A cursor it can read is read as the position it names: cursors are not signed, so an edited cursor that still names a position is answered with the page after that position, which may be empty. Pass back exactly the value you were given.
Stored and replayed a week later, a cursor resumes from a position in a sort that has since changed. That is not a failure you will see; it is rows you will not see. Hold a cursor for the length of one loop and take a fresh one next time.
Page size
limit defaults to 50 and accepts up to 200. A larger value is refused with a 400 rather than trimmed to fit, because a request for more than the API will serve is a bug in the client and should read as one.
| Parameter | Value |
|---|---|
limit, when you do not send one | 50 |
limit, the most you may ask for | 200 |
What comes back
Three fields, on every paged response, whatever the resource:
items- This page of rows.
nextCursor- The cursor for the page after this one, or null when there is no page after this one. Null is the end condition — not an empty array, and not a 404.
total- The count over the whole filtered list, not the size of this page, so it is a number you can show a reader.
How you know you are done
You are done when nextCursor is null. Not an empty array, not a 404.
A loop that waits for an empty page instead never gets one. The last page is full or short, its nextCursor is null, and a request sent without a cursor — or with one the API cannot read — is answered with the first page. So the loop starts again from the top and never ends.
A request that is refused rather than answered is an application/problem+json document, and the same three rate-limit headers are on it as on a successful response.
The loop
Both programs below read the critical issues of the first asset a key reaches, three to a page, and print each page’s titles. Run with a test key, they read the sandbox, and print the same thing.
TypeScript
typescript: page through an asset’s critical issues
// Every critical issue on the first asset a key reaches, three to a page.
// Node 22.18 or later runs this file as it is: node page-issues.ts
import { env } from 'node:process';
const BASE_URL = 'https://api.barrierops.com/api/v1';
const KEY = env['BARRIEROPS_API_KEY'];
if (KEY === undefined) {
throw new Error('Set BARRIEROPS_API_KEY to your API key first.');
}
interface Page<Item> {
items: Item[];
nextCursor: string | null;
total: number;
}
interface Asset {
id: string;
name: string;
}
interface Issue {
title: string;
viewport: string;
}
async function get<Body>(path: string): Promise<Body> {
const response = await fetch(`${BASE_URL}${path}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!response.ok) {
throw new Error(`${String(response.status)} from ${path}: ${await response.text()}`);
}
return (await response.json()) as Body;
}
const count = (n: number, noun: string): string => `${String(n)} ${noun}${n === 1 ? '' : 's'}`;
const [asset] = (await get<Page<Asset>>('/properties')).items;
if (asset === undefined) {
throw new Error('This key reaches no asset.');
}
let cursor: string | null = null;
let pages = 0;
let seen = 0;
do {
const query = new URLSearchParams({ impact: 'critical', limit: '3' });
if (cursor !== null) {
query.set('cursor', cursor);
}
const page: Page<Issue> = await get(`/properties/${asset.id}/issues?${query.toString()}`);
pages += 1;
seen += page.items.length;
console.log(`Page ${String(pages)}`);
for (const issue of page.items) {
console.log(` ${issue.title} (${issue.viewport})`);
}
cursor = page.nextCursor;
} while (cursor !== null);
console.log(`${count(seen, 'critical issue')} on ${asset.name}, in ${count(pages, 'page')}`);
Lines are wrapped for printing. The original is one line.
Python
python: page through an asset’s critical issues
# Every critical issue on the first asset a key reaches, three to a page.
# Python 3.8 or later, standard library only: python3 page_issues.py
import json
import os
import urllib.parse
import urllib.request
BASE_URL = "https://api.barrierops.com/api/v1"
KEY = os.environ["BARRIEROPS_API_KEY"]
def get(path):
request = urllib.request.Request(
BASE_URL + path, headers={"Authorization": f"Bearer {KEY}"}
)
with urllib.request.urlopen(request) as response:
return json.load(response)
def count(n, noun):
return f"{n} {noun}{'' if n == 1 else 's'}"
asset = get("/properties")["items"][0]
cursor = None
pages = 0
seen = 0
while True:
query = {"impact": "critical", "limit": 3}
if cursor is not None:
query["cursor"] = cursor
page = get(f"/properties/{asset['id']}/issues?" + urllib.parse.urlencode(query))
pages += 1
seen += len(page["items"])
print(f"Page {pages}")
for issue in page["items"]:
print(f" {issue['title']} ({issue['viewport']})")
cursor = page["nextCursor"]
if cursor is None:
break
print(f"{count(seen, 'critical issue')} on {asset['name']}, in {count(pages, 'page')}")
Lines are wrapped for printing. The original is one line.
What both print
text: the output of either program
Page 1
Input buttons must have discernible text (desktop)
Images must have alternative text (desktop)
Select element must have an accessible name (desktop)
Page 2
ARIA attributes must conform to valid values (desktop)
Form elements must have labels (desktop)
Images must have alternative text (desktop)
Page 3
Form elements must have labels (desktop)
Images must have alternative text (desktop)
Images must have alternative text (desktop)
Page 4
Buttons must have discernible text (desktop)
Buttons must have discernible text (desktop)
ARIA attributes must conform to valid values (desktop)
Page 5
Required ARIA attributes must be provided (desktop)
Elements must only use supported ARIA attributes (desktop)
Buttons must have discernible text (desktop)
15 critical issues on riverbend.example, in 5 pagesLines are wrapped for printing. The original is one line.
The first response
bash: ask for the first page
curl "https://api.barrierops.com/api/v1/properties/9ccf6432-ad06-5afe-8a30-52373a0346a4/issues?impact=critical&limit=3" \
-H "Authorization: Bearer $BARRIEROPS_API_KEY"Lines are wrapped for printing. The original is one line.
json: the complete first page
{
"items": [
{
"id": "274b937b-fb93-5927-9ba6-c81132eb1e9a",
"orgId": "b7749447-87f3-5d35-beee-99e152f35574",
"propertyId": "9ccf6432-ad06-5afe-8a30-52373a0346a4",
"title": "Input buttons must have discernible text",
"wcagSC": [
"4.1.2"
],
"impact": "critical",
"status": "open",
"outcome": "violation",
"reviewState": "pending",
"viewport": "desktop",
"assignee": null,
"occurrenceCount": 30,
"firstSeenAt": "2026-10-07T15:14:00.000Z",
"updatedAt": "2026-10-07T15:14:00.000Z"
},
{
"id": "0f20676e-b8ed-5a45-9719-424c620317b0",
"orgId": "b7749447-87f3-5d35-beee-99e152f35574",
"propertyId": "9ccf6432-ad06-5afe-8a30-52373a0346a4",
"title": "Images must have alternative text",
"wcagSC": [
"1.1.1"
],
"impact": "critical",
"status": "verified",
"outcome": "violation",
"reviewState": "pending",
"viewport": "desktop",
"assignee": {
"email": "marco.ellison@northfork-web.example",
"name": "Marco Ellison"
},
"occurrenceCount": 0,
"firstSeenAt": "2026-07-07T15:14:00.000Z",
"updatedAt": "2026-09-09T16:45:00.000Z"
},
{
"id": "1ebe1c69-5152-5ac7-a210-d086e8903655",
"orgId": "b7749447-87f3-5d35-beee-99e152f35574",
"propertyId": "9ccf6432-ad06-5afe-8a30-52373a0346a4",
"title": "Select element must have an accessible name",
"wcagSC": [
"4.1.2"
],
"impact": "critical",
"status": "ready_to_verify",
"outcome": "violation",
"reviewState": "pending",
"viewport": "desktop",
"assignee": null,
"occurrenceCount": 0,
"firstSeenAt": "2026-07-07T15:14:00.000Z",
"updatedAt": "2026-10-07T15:14:00.000Z"
}
],
"nextCursor": "eyJzb3J0IjoiZmlyc3RTZWVuQXQiLCJvcmRlciI6ImRlc2MiLCJkaXIiOiJhZnRlciIsInZhbHVlcyI6WyIyMDI2LTA3LTA3VDE1OjE0OjAwLjAwMDAwMFoiLCIxZWJlMWM2OS01MTUyLTVhYzctYTIxMC1kMDg2ZTg5MDM2NTUiXX0",
"total": 15
}Lines are wrapped for printing. The original is one line.
The last response
Asked for with the nextCursor the page before it returned. Its own nextCursor is null, and that is how the loop knows to stop.
bash: ask for the last page
curl "https://api.barrierops.com/api/v1/properties/9ccf6432-ad06-5afe-8a30-52373a0346a4/issues?impact=critical&limit=3&cursor=eyJzb3J0IjoiZmlyc3RTZWVuQXQiLCJvcmRlciI6ImRlc2MiLCJkaXIiOiJhZnRlciIsInZhbHVlcyI6WyIyMDI2LTA3LTA3VDE1OjE0OjAwLjAwMDAwMFoiLCJhYjM1YTgxZC0xYzYyLTVhYWYtYmY2ZC1mNDA5MDFjN2MyYWQiXX0" \
-H "Authorization: Bearer $BARRIEROPS_API_KEY"Lines are wrapped for printing. The original is one line.
json: the complete last page
{
"items": [
{
"id": "bad6fa32-6c5c-588d-905f-f267b7625788",
"orgId": "b7749447-87f3-5d35-beee-99e152f35574",
"propertyId": "9ccf6432-ad06-5afe-8a30-52373a0346a4",
"title": "Required ARIA attributes must be provided",
"wcagSC": [
"4.1.2"
],
"impact": "critical",
"status": "ready_to_verify",
"outcome": "violation",
"reviewState": "pending",
"viewport": "desktop",
"assignee": null,
"occurrenceCount": 0,
"firstSeenAt": "2026-07-07T15:14:00.000Z",
"updatedAt": "2026-10-07T15:14:00.000Z"
},
{
"id": "cf6e300f-3f90-5ee1-a1d5-b28cc03ce135",
"orgId": "b7749447-87f3-5d35-beee-99e152f35574",
"propertyId": "9ccf6432-ad06-5afe-8a30-52373a0346a4",
"title": "Elements must only use supported ARIA attributes",
"wcagSC": [
"4.1.2"
],
"impact": "critical",
"status": "open",
"outcome": "violation",
"reviewState": "pending",
"viewport": "desktop",
"assignee": null,
"occurrenceCount": 90,
"firstSeenAt": "2026-07-07T15:14:00.000Z",
"updatedAt": "2026-10-07T15:14:00.000Z"
},
{
"id": "e705c220-780d-57dd-b661-7c9b21a23a66",
"orgId": "b7749447-87f3-5d35-beee-99e152f35574",
"propertyId": "9ccf6432-ad06-5afe-8a30-52373a0346a4",
"title": "Buttons must have discernible text",
"wcagSC": [
"4.1.2"
],
"impact": "critical",
"status": "ready_to_verify",
"outcome": "violation",
"reviewState": "pending",
"viewport": "desktop",
"assignee": null,
"occurrenceCount": 0,
"firstSeenAt": "2026-07-07T15:14:00.000Z",
"updatedAt": "2026-10-07T15:14:00.000Z"
}
],
"nextCursor": null,
"total": 15
}Lines are wrapped for printing. The original is one line.