> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sigmora.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Page through list endpoints with limit and cursor, and resume ordered streams with after.

List endpoints return results in pages. Sigmora uses one convention for lists
and one for ordered streams.

## Lists: `limit` and `cursor`

| Name | Location | Description |
| - | - | - |
| `limit` | Query parameter | Maximum number of items to return. Each endpoint documents its default and maximum. |
| `cursor` | Query parameter | The `nextCursor` from the previous page. Omit it for the first page. |
| `nextCursor` | Response field | Send this as `cursor` to get the next page. `null` on the last page. |
| `hasMore` | Response field | Whether another page exists, on endpoints that return it. |

Cursors are opaque: pass them back exactly as you received them, and do not
parse, construct or store them long term. A cursor is bound to the query that
produced it, so keep the other filters the same while you page.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  # First page
  curl "https://api.sigmora.org/v1/projects?limit=50" \
    -H "Authorization: Bearer $SIGMORA_API_KEY"

  # Next page: send the previous response's nextCursor as cursor
  curl "https://api.sigmora.org/v1/projects?limit=50&cursor=eyJ2IjoxLCJrIjoi..." \
    -H "Authorization: Bearer $SIGMORA_API_KEY"
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import os

  import requests

  session = requests.Session()
  session.headers["Authorization"] = f"Bearer {os.environ['SIGMORA_API_KEY']}"

  def list_all(path: str, key: str, **params):
      cursor = None
      while True:
          page = session.get(
              f"https://api.sigmora.org{path}",
              params={**params, "limit": 50, **({"cursor": cursor} if cursor else {})},
              timeout=30,
          ).json()
          yield from page[key]
          cursor = page.get("nextCursor")
          if not cursor:
              return

  for project in list_all("/v1/projects", "projects", status="all"):
      print(project["id"], project["title"])
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  async function* listAll(path: string, key: string, params: Record<string, string> = {}) {
    let cursor: string | null = null;
    do {
      const query = new URLSearchParams({ ...params, limit: "50", ...(cursor ? { cursor } : {}) });
      const page = await fetch(`https://api.sigmora.org${path}?${query}`, {
        headers: { Authorization: `Bearer ${process.env.SIGMORA_API_KEY}` },
      }).then((r) => r.json());
      yield* page[key];
      cursor = page.nextCursor ?? null;
    } while (cursor);
  }

  for await (const project of listAll("/v1/projects", "projects", { status: "all" })) {
    console.log(project.id, project.title);
  }
  ```
</CodeGroup>

Stop when `nextCursor` is `null`, not when a page comes back empty. Some lists
filter by visibility after reading — for example, other members' private
projects — so a page can be empty while more results remain.

These endpoints use `cursor` and `nextCursor`:

| Endpoint | Items field |
| - | - |
| `GET /v1/projects` | `projects` |
| `GET /v1/assets` | `assets` |
| `GET /v1/jobs` | `jobs` |
| `GET /v1/themes` | `themes` |
| `GET /v1/campaigns` | `campaigns` |
| `GET /v1/autopilot-runs` | `runs` |
| `GET /v1/webhook-subscriptions` | `subscriptions` |
| `GET /v1/projects/{projectId}/variations/{batchId}` | `variants` |
| `GET /v1/projects/{projectId}/comments` | `comments` |
| `GET /v1/video/catalog/{domain}` | `videos` |

Other list endpoints return one bounded page controlled by `limit`; their
reference pages say so.

## Transcript words: `wordCursor`

`GET /v1/projects/{projectId}/transcript` pages the words of a composition's
transcript rather than a list of items. `wordLimit` (default 1000, at most
2000\) caps the words across all sources on one page; every source is listed on
every page with its `wordCount`, so merge the pages' `words` by `assetId`.
Follow `wordsNextCursor` by sending it back as `wordCursor`, until it is
`null`.

A `wordCursor` is bound to the transcript it paged. If the composition changes
between pages (an edit, a new source or transcript), the next page answers
`409 conflict` with `details.reason` `stale_cursor`: discard the pages you
have and read again from the first page.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://api.sigmora.org/v1/projects/prj_8f2k1m/transcript?wordLimit=1000&wordCursor=$WORDS_NEXT_CURSOR" \
  -H "Authorization: Bearer $SIGMORA_API_KEY"
```

## Ordered streams: `after`

A few endpoints return an ordered sequence you consume from the start and resume
later. For these, `after` is the **last position you have already processed**,
and the response tells you where to continue.

| Endpoint | `after` means | Continue with |
| - | - | - |
| `GET /v1/jobs/{jobId}/events` | The last event `sequence` you applied | `nextAfter`, until `hasMore` is `false` |
| `GET /v1/assets/{assetId}/transcript` | The number of transcript segments already read | The next offset, until no segments remain |
| `GET /v1/jobs/{jobId}/proposal` | The number of proposed cuts already read | `page.nextAfter`, until it is `null` |

Persist the position with your own state. After a crash or a dropped WebSocket,
resume from it and you will not miss or repeat an event.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://api.sigmora.org/v1/jobs/job_4k1b7d/events?after=42&limit=100" \
  -H "Authorization: Bearer $SIGMORA_API_KEY"
```

## Deprecated parameters

Earlier spellings still work, so existing integrations keep running, but new code
should use `cursor`:

| Endpoint | Deprecated | Use instead |
| - | - | - |
| `GET /v1/assets` | `after` query parameter, `nextAfter` response field | `cursor`, `nextCursor` |
| `GET /v1/projects/{projectId}/variations/{batchId}` | `after` query parameter, `nextAfter` response field | `cursor`, `nextCursor` |
| `GET /v1/video/catalog/{domain}` | `offset` query parameter | `cursor` (when both are sent, `cursor` wins) |

The API reference marks these parameters as deprecated. See
[Versioning](/versioning) for how deprecations work.

## Hook search

`GET /v1/hooks` and `GET /v1/hooks/stats` search a bounded, changing sample of
recent content rather than a stable collection. They page with `offset` and
return `nextOffset`, which is `null` at the end of the sample.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.