---
title: "List Reports"
method: GET
path: "/api/reports"
tags: ["reports"]
---

# List Reports

`GET /api/reports`

## Request

- Base URL: `https://api.parlel.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.parlel.com/api/reports`
- Auth: none declared

The caller's reports, newest first, one page at a time.

## ``agent_id``

Narrows the list to the reports cut from one agent, which is how the agent
page's Reports tab reads. Optional, and its absence means "everything I
own" -- the two callers want genuinely different lists rather than one being
a special case of the other.

Filtered in SQL rather than by fetching a page and dropping the rows that do
not match. That distinction is not premature: this list is keyset-paged, so a
client-side filter would return a *page* of twelve mixed reports and then
show however many of them happened to belong to the agent, which reads as
"this agent has three reports" when it has thirty. The predicate has to be
inside the window, not outside it.

An unknown or unowned ``agent_id`` is not an error. It returns an empty page,
because ``user_id`` is already in the WHERE clause and no row can escape it:
asking about somebody else's agent is answered with "you have no reports from
that", which is true, and which leaks nothing about whether the agent exists.

No index is added for this. ``ix_agent_reports_agent_id`` already exists on
the column, and the account-scoped row count here is in the tens.

## Why this grew a cursor

It used to select every report the account owned, with no limit and no
cursor, and render all of them. That is fine at nine reports and it is a page
that gets slower every time somebody uses the feature -- the failure mode is
not a crash, it is a screen that takes a little longer to open each month
until somebody calls it broken.

## Keyset, not offset

Same shape as the two public directories: order by ``(created_at DESC, id
DESC)`` and resume from a cursor naming a position in that order. ``id`` is
the tie-break, so the order is total and a page boundary cannot sit in the
middle of a group of reports created in the same second -- which is exactly
what happens when somebody generates two from the same agent.

Offset paging would re-read and skip rows, and would also drop a report from
page two the moment a new one is written between requests. A cursor names a
row, so a report arriving while the reader pages cannot shuffle the page they
have not read yet.

## The shape changed

This used to return a bare array. It now returns ``{items, next_cursor}``,
matching the directories, because a list with no envelope has nowhere to put
"there is more". `next_cursor` is `null` on the last page.

## Query parameters

- `limit` integer
- `cursor` string, nullable
- `agent_id` string, nullable

## Headers

- `authorization` string, nullable

## Response `200`

Successful Response

- object

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/parlel/apis/parlel-api.md) · [All operations](https://skmtc.dev/parlel/apis/parlel-api/llms.txt) · [OpenAPI document](https://skmtc.dev/parlel/apis/parlel-api/revisions/4d41a8f5cfc9?raw)
