Page through and filter lists

Every /v2 list pages the same way and returns the same envelope. Page with starting_after to read a whole list, repeat a list's id parameter to fetch specific records, and add filters to narrow the results. To mirror audits in your own system, see Keep audits in sync with updated_after.

Cursor pagination

Records come newest first, ordered by created_at then by id, both descending. There is no next_cursor: send the last record's id as starting_after until has_more is false.

Parameter Type Meaning
limit integer, 1 to 100 The page size, default 20. A value out of range returns 400 VALIDATION_ERROR; the API does not clamp limit=101 to 100.
starting_after UUID Records older than this id. The id comes from the list's own id field, such as audit_id on audits; a cursor from one list returns an empty page on another.
ending_before UUID Records newer than this id, still newest first. Send one cursor, never both: the API accepts the pair but uses only one.
include_total true or false Adds total_count, the exact number of matching records across all pages. Counting is slower, so ask for it only when you need it.

Each resource adds its own filters, combined with AND (Filters by resource). The API ignores an unknown parameter without an error, so a misspelled filter has no effect. To fetch several records by id, see Batch get by id.

The list envelope

This request asks for one complete audit and the total number of matches:

curl "https://api.brellium.com/v2/audits?status=complete&limit=1&include_total=true" \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams({
  status: "complete",
  limit: "1",
  include_total: "true",
});
const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
  headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.error)}`);
console.log(body);
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

const audits = await client.audits.list({
  status: "complete",
  limit: 1,
  include_total: "true",
});
console.log(audits);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

audits = client.audits.list(status="complete", limit=1, include_total="true")
print(audits.to_json())
{
  "object": "list",
  "data": [
    {
      "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "object": "audit",
      "status": "complete",
      "created_at": "2026-09-24T02:34:54.489Z"
      /* ... the full audit object: Understand an audit */
    }
  ],
  "has_more": true,
  "total_count": 342
}

Each item in data is the full record, the same one the retrieve endpoint returns (Understand an audit). total_count appears only with include_total=true. When nothing matches, the response is 200 with { "object": "list", "data": [], "has_more": false }.

A worked example

Five audits created in the order A to E list as E, D, C, B, A. With limit=2, three requests page through them:

Request data has_more
GET /v2/audits?limit=2 E, D true
GET /v2/audits?limit=2&starting_after=<id of D> C, B true
GET /v2/audits?limit=2&starting_after=<id of B> A false

GET /v2/audits?limit=2&ending_before=<id of C> pages the other way and returns E and D, the records newer than C, still newest first.

Here is the second request. Its ids are made up for the example: the first hex digit matches the letter, so d4e5... is D. The last record in the response, B, gives the next starting_after:

curl --get "https://api.brellium.com/v2/audits" \
  --data-urlencode "limit=2" \
  --data-urlencode "starting_after=d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80" \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams({
  limit: "2",
  starting_after: "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
});
const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
  headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.error)}`);
console.log(body);
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

const audits = await client.audits.list({
  limit: 2,
  starting_after: "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
});
console.log(audits);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

audits = client.audits.list(
    limit=2,
    starting_after="d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
)
print(audits.to_json())

created_at never changes, so an update does not move a record while you page. The pagination loop never repeats a record. It returns every record that was in the list at the start, as long as the record still matches your filters when the loop requests its page.

Records created after the start land in front of the first page, so paging forward with starting_after never reaches them. Lists can also take a few seconds to include a new or changed record. To pick up both, poll with overlapping updated_after windows as in Keep audits in sync with updated_after.

The pagination loop

Each loop below requests limit=100 (the largest page, so the fewest requests) and stops when has_more is false. The curl loop prints one JSON line per audit, and the TypeScript and Python loops yield each audit to your code. Send your filters with every request: the cursor is only an id and carries no filters.

after=""
while :; do
  url="https://api.brellium.com/v2/audits?limit=100&status=complete"
  if [ -n "$after" ]; then url="$url&starting_after=$after"; fi
  # --fail stops the loop on a 401 or 429 instead of ending it as if done.
  page=$(curl --silent --fail "$url" \
    --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN") || break
  echo "$page" | jq -c '.data[] | {audit_id, status, score}'
  if [ "$(echo "$page" | jq -r '.has_more')" != "true" ]; then break; fi
  after=$(echo "$page" | jq -r '.data[-1].audit_id')
done
{"audit_id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","status":"complete","score":0.6666666666666666}
{"audit_id":"c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f","status":"complete","score":1}
{"audit_id":"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e","status":"complete","score":0.5}
// Keep credentials on the server; never in a browser or mobile app.
const token = process.env.BRELLIUM_ACCESS_TOKEN;
if (!token) throw new Error("BRELLIUM_ACCESS_TOKEN is not set");

type Audit = {
  audit_id: string;
  status: string;
  score: number | null;
  created_at: string;
};
type Page<T> = {
  object: "list";
  data: T[];
  has_more: boolean;
  total_count?: number;
};

async function* listAudits(
  filters: Record<string, string> = {},
): AsyncGenerator<Audit> {
  let startingAfter: string | undefined;
  while (true) {
    const params = new URLSearchParams({ limit: "100", ...filters });
    if (startingAfter) params.set("starting_after", startingAfter);
    const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok)
      throw new Error(`GET /v2/audits failed: ${res.status} ${await res.text()}`);
    const page = (await res.json()) as Page<Audit>;
    for (const audit of page.data) yield audit;
    if (!page.has_more) return;
    startingAfter = page.data[page.data.length - 1].audit_id;
  }
}

async function main() {
  for await (const audit of listAudits({ status: "complete" })) {
    console.log(audit.audit_id, audit.status, audit.score);
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

// list() returns one page; send your filters again with every starting_after.
let startingAfter: string | undefined;
while (true) {
  const page = await client.audits.list({
    limit: 100,
    status: "complete",
    starting_after: startingAfter,
  });
  for (const audit of page.data)
    console.log(audit.audit_id, audit.status, audit.score);
  if (!page.has_more) break;
  startingAfter = page.data[page.data.length - 1].audit_id;
}
import os
from typing import Union

from brellium_api_client import BrelliumClient, Omit, omit

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

# list() returns one page; send your filters again with every starting_after.
starting_after: Union[str, Omit] = omit
while True:
    page = client.audits.list(
        limit=100, status="complete", starting_after=starting_after
    )
    for audit in page.data:
        print(audit.audit_id, audit.status, audit.score)
    if not page.has_more:
        break
    starting_after = page.data[-1].audit_id
# Keep credentials on the server; never in a browser or mobile app.
import os

import requests

TOKEN = os.environ["BRELLIUM_ACCESS_TOKEN"]
BASE_URL = "https://api.brellium.com"


def iter_audits(session, **filters):
    params = {"limit": 100, **filters}
    while True:
        res = session.get(f"{BASE_URL}/v2/audits", params=params, timeout=30)
        res.raise_for_status()
        page = res.json()
        yield from page["data"]
        if not page["has_more"]:
            return
        params["starting_after"] = page["data"][-1]["audit_id"]


session = requests.Session()
session.headers["Authorization"] = f"Bearer {TOKEN}"
for audit in iter_audits(session, status="complete"):
    print(audit["audit_id"], audit["status"], audit["score"])

If a request inside the loop returns 429 RATE_LIMITED, wait Retry-After seconds, repeat the same request and continue the loop (Rate limits and errors).

Batch get by id

Repeat the list's id parameter, named after the id field, or send the ids in one value separated by commas (?audit_id=<id>,<id>):

List Parameter
GET /v2/audits audit_id
GET /v2/documents document_id
GET /v2/coding_evaluations coding_eval_id
GET /v2/users user_id
GET /v2/question_sets set_id
GET /v2/email_recipients person_id

This request fetches two audits by id:

curl --get "https://api.brellium.com/v2/audits" \
  --data-urlencode "audit_id=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
  --data-urlencode "audit_id=b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" \
  --data-urlencode "limit=100" \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams();
params.append("audit_id", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
params.append("audit_id", "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e");
params.append("limit", "100");
const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
  headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.error)}`);
console.log(body);
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

const audits = await client.audits.list({
  audit_id: [
    "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
  ],
  limit: 100,
});
console.log(audits);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

audits = client.audits.list(
    audit_id=[
        "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
    ],
    limit=100,
)
print(audits.to_json())
{
  "object": "list",
  "data": [
    {
      "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "object": "audit",
      "status": "complete"
    }, // one item per id you asked for
    {
      "audit_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
      "object": "audit",
      "status": "complete"
    } // remaining audit fields omitted
  ],
  "has_more": false
}

The response is a list page with the audits you asked for. These rules apply to every batch get:

  • The id parameter is a filter, so limit still applies. Send a limit of at least the number of ids, or the page stops early with has_more: true.
  • An id that matches no record in your organization is left out, with no error. The response is still 200, so compare what came back with what you asked for.
  • ?audit_id=<id>,<id> is the same request as ?audit_id=<id>&audit_id=<id>, and you can mix the two forms. Only the batch get id parameters and the audits documents filter take commas; in flags, a comma is part of the flag.
  • A duplicated id returns the record once.
  • An id that is not a UUID returns 400 VALIDATION_ERROR, and so does an empty value such as the one after a trailing comma.
  • A query string over 2,048 bytes is rejected with a bare 403 and no JSON body, so about 44 audit_id values fit in one request; split larger batches. audit_id=<uuid>& takes 46 bytes, and fewer ids fit with a longer name such as coding_eval_id or with other filters. A comma-separated id takes 37 bytes, so about 50 fit. The documents filter on audits has the same limit, so its maximum of 100 ids never fits in one request.

Filters by resource

Every filter is an exact match unless its table row says otherwise. Timestamp filters are inclusive: created_after=T means created_at >= T, and created_before=T means created_at <= T.

Send a + in a query value as %2B: a raw + decodes to a space and matches nothing. @ needs no encoding.

Audits

GET /v2/audits (List audits) takes these filters:

Parameter Type Meaning
status string Audits with this exact status, for example complete; the match is case-sensitive.
set_id UUID Audits scored against this question set.
flags string, repeatable (up to 50) Audits with at least one of the given flags, matched exactly and case-sensitively: ?flags=Failed&flags=Missing%20Signature matches either flag.
documents UUID, repeatable or comma-separated (1 to 100) Audits covering any of these document_id values: ?documents=0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b&documents=11111111-1111-4111-8111-111111111111.
external_id string, 1 to 255 characters Audits covering a document with this external_id (your own reference id).
created_after, created_before RFC 3339 Inclusive bounds on created_at.
updated_after, updated_before RFC 3339 Inclusive bounds on updated_at; Keep audits in sync with updated_after uses them.

There is no filter on people, so match audits to people in your own code (Link an audit's people to recipients and users). For every audit of one document, see List a document's audits.

Documents

GET /v2/documents (List documents) takes these filters:

Parameter Type Meaning
group_id UUID Documents of one POST /v2/documents request (one batch), including those unpacked from a zip in it.
status string Documents with this status, for example pending_upload, todo, complete or upload_expired (Upload documents).
doc_type string Documents with this doc_type.
provider string Documents with this provider.
client string Documents with this client.
created_after, created_before RFC 3339 Inclusive bounds on created_at.

The documents list has no updated_after or external_id filter. To find a document's audits, use documents= or external_id= on GET /v2/audits.

Coding evaluations

GET /v2/coding_evaluations (List coding evaluations) takes these filters:

Parameter Type Meaning
status string Coding evaluations with this status.
audit_id UUID Coding evaluations of this audit.
document_id UUID Coding evaluations covering this document. It takes one id, unlike the repeatable documents filter on audits.
created_after, created_before RFC 3339 Inclusive bounds on created_at.
updated_after, updated_before RFC 3339 Inclusive bounds on updated_at.

Users

GET /v2/users (List users) takes these filters:

Parameter Type Meaning
email string The user with this email address, ignoring case.
created_after, created_before RFC 3339 Inclusive bounds on created_at.

The list never includes deleted users. It has no updated_after filter.

Question sets

GET /v2/question_sets (List question sets) takes this filter:

Parameter Type Meaning
type string Question sets with this type, matched exactly and case-sensitively. A set with a null type never matches.

The list leaves out deleted question sets and has no timestamp filters.

Email recipients

GET /v2/email_recipients (List email recipients) takes these filters:

Parameter Type Meaning
email string Email recipients with this email address, ignoring case. Older recipients can share an address, so data can hold more than one item.
created_after, created_before RFC 3339 Inclusive bounds on created_at.

Deleted email recipients never appear in the list. A deleted recipient's person_id still works as a starting_after or ending_before cursor, so a loop that deletes while it pages still reaches the end.

Timestamps

Every created_at and updated_at is RFC 3339 in UTC with millisecond precision and a trailing Z, such as 2026-09-24T02:34:54.489Z. Filters accept the same form with or without fractional seconds, so 2026-09-24T02:34:54Z works. A date alone (2026-09-24) or an offset (2026-09-24T02:34:54+02:00) returns 400 VALIDATION_ERROR with the message Invalid ISO datetime, so send a full UTC timestamp ending in Z (Common validation errors). display_info[].value on an audit or coding evaluation is a display string and can have another format.

Cursor edge cases

A cursor UUID that matches no record in the list returns an empty page with 200. If a loop ends early, check that the stored cursor is an id from the list you are paging. A malformed cursor such as starting_after=not-a-uuid returns 400 VALIDATION_ERROR, with "message": "Invalid UUID" in details.

Next