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

# Pagination

> Cursor-based pagination: follow next_cursor while has_more is true.

Financial Data API list endpoints paginate with opaque cursors. You request a page, read `meta.pagination`, and follow `next_cursor` until `has_more` is `false`. Cursors are stable for the result set you started, so paging stays consistent even as new observations arrive.

This page covers the query parameters, the `meta.pagination` block, and a complete loop in cURL and TypeScript.

## Query parameters

All list endpoints accept the same paging controls.

<ParamField query="limit" type="integer" default="100">
  Page size, from 1 to 500. Defaults to 100. Larger pages mean fewer round trips; smaller pages mean lower latency per request.
</ParamField>

<ParamField query="cursor" type="string">
  An opaque cursor that marks where the next page begins. Omit it on your first request. On every subsequent request, pass the `next_cursor` value from the previous response. Do not construct or parse cursor values: treat them as opaque tokens.
</ParamField>

<ParamField query="sort" type="string">
  The field to sort by. The set of sortable fields depends on the endpoint. Consult the [OpenAPI spec](https://api.financialdatapi.com/openapi.json) for what each endpoint supports.
</ParamField>

<ParamField query="order" type="string" default="asc">
  Sort direction: `asc` or `desc`. Defaults to `asc`.
</ParamField>

<Warning>
  Unknown query parameters are rejected, not ignored. A misspelled parameter returns `bad_request`. See [Errors](/concepts/errors#fail-closed-on-unknown-parameters).
</Warning>

## The pagination block

List responses carry the cursor state under `meta.pagination`. (Item responses do not paginate and omit this block.)

<ResponseField name="meta.pagination.limit" type="integer">
  The page size applied to this request.
</ResponseField>

<ResponseField name="meta.pagination.cursor" type="string | null">
  The cursor you supplied for this page, or `null` on the first page.
</ResponseField>

<ResponseField name="meta.pagination.next_cursor" type="string | null">
  The cursor to pass on your next request. `null` when there are no more pages.
</ResponseField>

<ResponseField name="meta.pagination.has_more" type="boolean">
  `true` when more pages remain. Loop while this is `true`.
</ResponseField>

```json First page response (truncated) theme={"theme":"css-variables"}
{
  "data": [ /* up to `limit` observations */ ],
  "meta": {
    "request_id": "8f1c2e90-7a4b-4c3d-9e21-2b6f0a1c4d55",
    "requestId": "8f1c2e90-7a4b-4c3d-9e21-2b6f0a1c4d55",
    "api_version": "v1",
    "pagination": {
      "limit": 100,
      "cursor": null,
      "next_cursor": "eyJvIjoxMDB9",
      "has_more": true
    }
  },
  "requestId": "8f1c2e90-7a4b-4c3d-9e21-2b6f0a1c4d55"
}
```

## The loop

The rule is one line: while `meta.pagination.has_more` is `true`, repeat the request with `cursor` set to the previous `next_cursor`. Stop when `has_more` is `false` (equivalently, when `next_cursor` is `null`).

<Steps>
  <Step title="Request the first page">
    Call the endpoint with your filters and an optional `limit`. Do not send `cursor`.
  </Step>

  <Step title="Process the page">
    Read the items from `data`.
  </Step>

  <Step title="Check has_more">
    If `meta.pagination.has_more` is `false`, you are done.
  </Step>

  <Step title="Advance the cursor">
    Otherwise, request again with `cursor` set to `meta.pagination.next_cursor`, keeping the same filters, and return to step 2.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={"theme":"css-variables"}
  # Walk every page of USA CPI observations, 500 rows at a time.
  BASE="https://api.financialdatapi.com/observations"
  QUERY="country=USA&indicator=cpi_inflation_yoy&limit=500"
  CURSOR=""

  while : ; do
    if [ -z "$CURSOR" ]; then
      URL="$BASE?$QUERY"
    else
      URL="$BASE?$QUERY&cursor=$CURSOR"
    fi

    RESP=$(curl -s -H "x-api-key: $FINANCIALDATA_API_KEY" "$URL")

    echo "$RESP" | jq '.data[]'   # process the page

    HAS_MORE=$(echo "$RESP" | jq -r '.meta.pagination.has_more')
    if [ "$HAS_MORE" != "true" ]; then
      break
    fi
    CURSOR=$(echo "$RESP" | jq -r '.meta.pagination.next_cursor')
  done
  ```

  ```python Python (requests) theme={"theme":"css-variables"}
  import os
  import requests

  BASE = "https://api.financialdatapi.com/observations"
  HEADERS = {"x-api-key": os.environ["FINANCIALDATA_API_KEY"]}
  params = {"country": "USA", "indicator": "cpi_inflation_yoy", "limit": 500}

  cursor = None
  while True:
      page_params = {**params, **({"cursor": cursor} if cursor else {})}
      resp = requests.get(BASE, headers=HEADERS, params=page_params)
      resp.raise_for_status()
      body = resp.json()

      for obs in body["data"]:
          ...  # process each observation

      page = body["meta"]["pagination"]
      if not page["has_more"]:
          break
      cursor = page["next_cursor"]
  ```

  ```typescript TypeScript (@financialdatapi/client) theme={"theme":"css-variables"}
  import { FinancialDataApiClient } from "@financialdatapi/client";

  const client = new FinancialDataApiClient({ apiKey: process.env.FINANCIALDATA_API_KEY! });

  // The SDK auto-paginates: async iteration transparently follows
  // next_cursor while has_more is true, so you never touch a cursor.
  for await (const obs of client.paginate("/observations", {
    country: "USA",
    indicator: "cpi_inflation_yoy",
    limit: 500,
  })) {
    // process each observation across all pages
    console.log(obs.period, obs.actual);
  }
  ```
</CodeGroup>

<Tip>
  The official TypeScript SDK `@financialdatapi/client` handles the cursor loop for you. Async iteration walks every page behind the scenes, so the manual loop above is only needed when you call the API directly.
</Tip>

## Notes

* Cursors are opaque. Their internal format is not part of the contract and may change. Read them from `next_cursor` and pass them back verbatim.
* Keep your filters identical across pages. Changing `country`, `indicator`, `sort`, or `order` mid-walk invalidates the cursor.
* A page may contain fewer than `limit` items and still have `has_more: true`. Always trust `has_more`, never the item count, to decide whether to continue.

<Card title="Response envelope" icon="box" href="/concepts/response-envelope">
  Where `meta.pagination` lives within the shared envelope.
</Card>
