# Paging and filters

Lists are paged by keyset cursor, not by offset. A cursor points at a position in the ordering rather than counting rows, so a story published while you page does not shift the page under you or make you miss a record.

## Cursors

Read a page, do the work, then send `nextCursor` back as `cursor` with **every other parameter unchanged**. Changing a filter mid-walk invalidates the cursor and returns `400 bad_request`. `nextCursor` is `null` on the final page.

The value is opaque. Do not parse it, store it as a sort key, or construct one.

```javascript Walking every page
const key = process.env.BABA_NEWS_API_KEY;
let cursor = null;

do {
  const params = new URLSearchParams({ lang: 'en', state: 'blindspot', limit: '50' });
  if (cursor) params.set('cursor', cursor);

  const res = await fetch(`https://news.itsbaba.com/api/v1/stories?${params}`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, Number(res.headers.get('Retry-After') ?? 60) * 1000));
    continue;
  }
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error.code}`);

  const page = await res.json();
  for (const story of page.data) handle(story);
  cursor = page.nextCursor;
} while (cursor);
```

## Filters

| Parameter | Range | Notes |
| --- | --- | --- |
| `lang` | `en`, `he` | Output language. Hebrew falls back to English field by field where a translation is missing. Anything that is not `he` or `iw` reads as `en`. |
| `state` | One state id | `breaking`, `developing`, `consensus`, `blindspot`, `undercovered`, `thread_update`, `quiet`, `resolved`. |
| `topic` | Repeatable | `politics`, `economy`, `tech`, `security`, `health`, `culture`, `sports`, `world`, `weather`, `general`. |
| `source` | Repeatable | Newsroom keys, as returned by the sources endpoint. |
| `since` | 0 to 30 | Jerusalem calendar days back, today inclusive. Above 30 is clamped to 30. |
| `limit` | 1 to 100 | Page size, default 40. Above 100 is clamped, and the clamped value comes back as `limit`. |
| `cursor` | Opaque | `nextCursor` from the previous page, with every other filter unchanged. |
| `q` | 2 to 120 chars | Search only. Sending it to the stories endpoint is a `400`. |

Repeatable parameters are OR'd within themselves and AND'd across: `topic=politics&topic=economy&source=ynet` means politics or economy, filed by Ynet.

## Conditional reads

Every read carries an `ETag`. Send it back as `If-None-Match` and an unchanged resource answers `304` with no body. It still counts as a request against quota, but it costs no bandwidth and no parsing.

```bash Conditional read
curl --get 'https://news.itsbaba.com/api/v1/brief' \
  --header "Authorization: Bearer $BABA_NEWS_API_KEY" \
  --header 'If-None-Match: "9f2c...c41"' \
  --dump-header -
```

## The story object

These fields appear on every story, in lists and in detail.

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | integer | Cluster id. The path segment for the story detail endpoint. |
| `url` | string | Canonical baba URL for the story. |
| `headline` | string | Lead headline in the requested language. |
| `summary` | string \| null | Machine-written summary of the cluster. |
| `topic` | string | Desk id. |
| `lang` | `en` \| `he` | Language of this payload, after fallback. |
| `publishedAt` | string | ISO 8601 instant the lead article was published. |
| `latestAt` | string | ISO 8601 instant of the newest article in the cluster. The sort key. |
| `updatedAt` | string \| null | When the lead body last changed. Null unless it changed more than 60 seconds after publish. |
| `outlets` | integer | Number of newsrooms in the cluster. |
| `sides` | object | Count of articles per press side, keyed by side. |
| `missingSides` | string[] | Sides that published nothing on this story. |
| `state` | string \| null | Cluster state. Null when none has been computed. |
| `verdict` | string \| null | Editorial label attached to the state, when there is one. |
| `lean` | object | Counts of left, center, right and unrated newsrooms. |
| `sources` | string[] | Newsroom keys in the cluster. Each is a key for the sources endpoints. |
| `thread` | object \| null | Running thread this story belongs to: `id`, `title`, `days`. |

Story detail adds `members` (one record per article), `timeline` (`firstAt`, `lastAt`, `spreadMinutes`, `firstSite`, and the first timestamp per side), `framingNote`, and an `outlets` counted from the members it returns.
