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

# Pagination

> List endpoints use cursor pagination with a Stripe-style startingAfter / endingBefore pattern. No page numbers, no deep-pagination cliff.

**What this is.** Every v3 list endpoint returns results in chunks. You navigate forward or backward using an **opaque cursor** the server returns to you. Cursors are tokens — treat them as strings, pass them back unchanged.

**When it matters.** Any time you list trades, deposits, or withdrawals. Two common patterns: "load all records" (back-fill into your DB) and "fetch what's new since I last polled" (continuous sync).

**What you'll learn.** The request parameters, response envelope, forward and backward iteration patterns, and why page numbers (and total counts) don't exist here.

All v3 list endpoints ([`GET /v3/fx/trades`](/v3/api-reference/trade/list-trades), [`GET /v3/fx/deposits`](/v3/api-reference/trade-settlement/list-deposits), [`GET /v3/fx/withdrawals`](/v3/api-reference/trade-settlement/list-withdrawals)) use cursor pagination. Results are sorted **newest-first by `createdAt`** and you page through them using cursors returned in the response.

Default page size is **25**; maximum is **100**.

## Request parameters

| Param           | Type                 | Default | Notes                                                               |
| --------------- | -------------------- | ------- | ------------------------------------------------------------------- |
| `limit`         | integer (1–100)      | 25      | Max records per page                                                |
| `startingAfter` | opaque cursor string | none    | Return records **older** than the cursor's position (next page)     |
| `endingBefore`  | opaque cursor string | none    | Return records **newer** than the cursor's position (previous page) |

`startingAfter` and `endingBefore` are mutually exclusive. Set one or the other, not both. On the first request, omit both — you get the newest page.

<Note>
  **Cursors are opaque.** The cursor value the server returns is a string token.
  Treat it as opaque — do not decode, trim, re-encode, parse, or substitute its
  contents. Pass it back verbatim. The token's internal format is an
  implementation detail and OpenFX reserves the right to change it.
</Note>

<Tip>
  **Mental model.** Lists are newest-first, so "starting after this cursor"
  means "older than the records this cursor points at" and "ending before this
  cursor" means "newer than the records this cursor points at". Think of the
  cursor as a position on a vertical timeline with newest at the top.
</Tip>

## Response envelope

Paginated responses wrap the array of records in a `pagination` envelope that carries the cursors for navigation:

```json theme={null}
{
  "data": [],
  "pagination": {
    "limit": 25,
    "hasNext": true,
    "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTAyLTA3VDEzOjAyOjI5LjE0N1oiLCJpZCI6IjZhOGRmZjAzLWFiY2QtNDEyMy1iYWNkLTAxMjM0NTY3ODlhYiJ9",
    "hasPrev": false,
    "prevCursor": null
  }
}
```

| Field                   | Type             | Notes                                                                                                                   |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `data`                  | array            | The records on this page, newest-first.                                                                                 |
| `pagination.limit`      | integer          | The `limit` the server used (echoes your request or the default).                                                       |
| `pagination.hasNext`    | boolean          | `true` if more records exist after this page — the signal to keep paginating forward. `false` on the last page.         |
| `pagination.nextCursor` | `string \| null` | Opaque token for the next page (older records); pass to `?startingAfter=`. `null` exactly when `hasNext` is `false`.    |
| `pagination.hasPrev`    | boolean          | `true` if records exist before this page — the signal to keep paginating backward. `false` on the first page.           |
| `pagination.prevCursor` | `string \| null` | Opaque token for the previous page (newer records); pass to `?endingBefore=`. `null` exactly when `hasPrev` is `false`. |

`hasNext` / `hasPrev` are the booleans you branch on; `nextCursor` / `prevCursor` are the tokens you pass back. A page with both `hasNext: false` and `hasPrev: false` is the entire paginated result set.

<Note>
  **Bounded lists do not paginate.** `GET /v3/fx/pairs`, `GET /v3/fx/balances`,
  and `GET /v3/fx/withdrawal-accounts` return their complete `data` array
  without cursor parameters or a `pagination` object. Apply the cursor flow on
  this page only to `trades`, `deposits`, and `withdrawals`.
</Note>

<Note>
  **What's not in the response.** `total`, `total_count`, and `total_pages` are
  intentionally not part of the envelope — running a `COUNT(*)` on every list
  call would defeat the cursor-seek index plan that keeps list endpoints fast at
  scale. Use `hasNext` / `hasPrev` to detect whether more pages exist; there is
  no total-count or page-number field.
</Note>

### Decoding cursors

**Don't.** Cursors are opaque — clients should never decode, parse, trim, re-encode, or substitute them. The cursor format is an implementation detail of the server, and OpenFX reserves the right to change it at any time without notice (different encoding, different payload fields, different length). Code that decodes cursors will break the day the encoding changes.

The contract is: **the server returns a cursor string; the client passes that same string back verbatim** on the next request via `?startingAfter=` or `?endingBefore=`. Round-trip it as opaque bytes.

### Cursor direction visualized

The timeline below shows a list of trades with the newest at the top. Suppose `pagination.nextCursor` from a previous response is currently positioned at trade `C` (highlighted). Each cursor parameter selects a different window:

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'13px','lineColor':'#299f68','primaryColor':'#ffffff','primaryTextColor':'#114330','primaryBorderColor':'#299f68'}}}%%
flowchart TB
  N["⬆ newest"]:::label --> A["A · 10:05"]
  A --> B["B · 10:04"]
  B --> C["C · 10:03  ← cursor position"]:::cursor
  C --> D["D · 10:02"]
  D --> E["E · 10:01"]
  E --> O["⬇ oldest"]:::label

  classDef label fill:#f0faf5,stroke:#299f68,stroke-width:1px,color:#114330
  classDef cursor fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
```

| Request                        | Returns                                                   | When to use                                                 |
| ------------------------------ | --------------------------------------------------------- | ----------------------------------------------------------- |
| `?endingBefore=<cursor at C>`  | `[A, B]` (newer than C, newest-first within the window)   | "Fetch what's new since I last saw trade C."                |
| `?startingAfter=<cursor at C>` | `[D, E]` (older than C, newest-first within the window)   | "Continue paginating into history past trade C."            |
| `?startingAfter=<cursor at E>` | `[]` with `pagination.hasNext: false`, `nextCursor: null` | You've reached the oldest record — nothing older than E.    |
| `?endingBefore=<cursor at A>`  | `[]` with `pagination.hasPrev: false`, `prevCursor: null` | You're already at the newest record — nothing newer than A. |

The records the cursor refers to are **not** included in the result page — `startingAfter` returns records older than the cursor's position; `endingBefore` returns records newer than it.

## Worked example: fetch all trades (forward iteration)

Walk forward (newest → oldest) until `hasNext` is `false`.

JWT TTL is 60s; a full back-fill across thousands of records can outlive it, so each of these signs per page rather than capturing one token — see [Rate limiting → Handling rate limits pattern](/v3/rate-limiting#handling-rate-limits-pattern).

<CodeGroup>
  ```javascript Node theme={null}
  // Walks GET /v3/fx/trades forward (newest -> oldest) until hasNext is
  // false — the pattern from /v3/pagination#worked-example-fetch-all-trades-forward-iteration.
  // signRequest is the real helper from /v3/authentication — adjust this path
  // if you've copied it somewhere else in your own project.
  const { signRequest } = require("../../authentication/node/sign-request");
  // Fetches one page. Takes signRequest and the HTTP call as parameters so
  // the cursor-walking logic below is testable without a real request.
  async function fetchPage(signRequest, cursor) {
    const url = new URL("https://api.openfx.com/v3/fx/trades");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("startingAfter", cursor);
    // Sign per page: the wall-clock to walk all pages is unbounded by the
    // JWT's 60s TTL.
    const { headers } = signRequest({ method: "GET", url: url.toString() });
    const res = await fetch(url, { headers });
    return res.json();
  }
  // Walks forward until hasNext is false. Takes fetchPage as a parameter so
  // the cursor-threading and termination logic is testable without a real
  // request or real credentials.
  async function* allTrades(signRequest, fetchPageFn = fetchPage) {
    let cursor = null;
    while (true) {
      const page = await fetchPageFn(signRequest, cursor);
      for (const trade of page.data) yield trade;
      if (!page.pagination.hasNext) return;
      cursor = page.pagination.nextCursor; // opaque token, round-trip verbatim
    }
  }
  module.exports = { allTrades, fetchPage };
  if (require.main === module) {
    (async () => {
      try {
        for await (const trade of allTrades(signRequest)) {
          console.log(trade.id, trade.createdAt);
        }
      } catch (err) {
        console.error("Error:", err.message);
        process.exitCode = 1;
      }
    })();
  }
  ```

  ```python Python theme={null}
  """Walks GET /v3/fx/trades forward (newest -> oldest) until hasNext is
  false — the pattern from
  /v3/pagination#worked-example-fetch-all-trades-forward-iteration."""
  import os
  import sys
  # sign_request is the real helper from /v3/authentication — adjust this path
  # if you've copied it somewhere else in your own project.
  sys.path.insert(
      0, os.path.join(os.path.dirname(__file__), "..", "..", "authentication", "python")
  )
  from sign_request import sign_request  # noqa: E402
  import requests  # noqa: E402
  def fetch_page(sign_request_fn, cursor):
      """Fetches one page. Takes sign_request as a parameter so the
      cursor-walking logic below is testable without a real request."""
      params = {"limit": 100}
      if cursor:
          params["startingAfter"] = cursor
      url = "https://api.openfx.com/v3/fx/trades"
      prepared = requests.Request("GET", url, params=params).prepare()
      # Sign per page: the wall-clock to walk all pages is unbounded by the
      # JWT's 60s TTL.
      headers, _ = sign_request_fn("GET", prepared.url, b"")
      return requests.get(prepared.url, headers=headers).json()
  def all_trades(sign_request_fn, fetch_page_fn=fetch_page):
      """Walks forward until hasNext is false. Takes fetch_page as a parameter
      so the cursor-threading and termination logic is testable without a
      real request or real credentials."""
      cursor = None
      while True:
          page = fetch_page_fn(sign_request_fn, cursor)
          for trade in page["data"]:
              yield trade
          if not page["pagination"]["hasNext"]:
              return
          cursor = page["pagination"]["nextCursor"]  # opaque token, round-trip verbatim
  if __name__ == "__main__":
      for trade in all_trades(sign_request):
          print(trade["id"], trade["createdAt"])
  ```

  ```java Java theme={null}
  // Walks GET /v3/fx/trades forward (newest -> oldest) until hasNext is
  // false — the pattern from
  // /v3/pagination#worked-example-fetch-all-trades-forward-iteration.
  import java.net.HttpURLConnection;
  import java.net.URI;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.function.BiFunction;
  public class AllTrades {
      // A minimal page shape: just the two fields the walking logic needs.
      static class Page {
          final List<String> tradeIds;
          final boolean hasNext;
          final String nextCursor;
          Page(List<String> tradeIds, boolean hasNext, String nextCursor) {
              this.tradeIds = tradeIds;
              this.hasNext = hasNext;
              this.nextCursor = nextCursor;
          }
      }
      // Named Signer, not SignRequest, to avoid colliding with the real
      // SignRequest class from /v3/authentication, compiled alongside this
      // file on the classpath.
      interface Signer {
          Map<String, String> signRequest(String method, String url, byte[] body) throws Exception;
      }
      // Fetches one page. Takes signer as a parameter so the cursor-walking
      // logic below is testable without a real request.
      static Page fetchPage(Signer signer, String cursor) throws Exception {
          String url = "https://api.openfx.com/v3/fx/trades?limit=100"
                  + (cursor != null ? "&startingAfter=" + cursor : "");
          // Sign per page: the wall-clock to walk all pages is unbounded by
          // the JWT's 60s TTL.
          Map<String, String> headers = signer.signRequest("GET", url, new byte[0]);
          HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
          conn.setRequestMethod("GET");
          for (Map.Entry<String, String> h : headers.entrySet()) {
              conn.setRequestProperty(h.getKey(), h.getValue());
          }
          conn.getResponseCode();
          // A real implementation would parse the JSON response body here,
          // using whatever JSON library you're already using.
          return new Page(List.of(), false, null);
      }
      // Walks forward until hasNext is false. Takes fetchPage as a parameter
      // so the cursor-threading and termination logic is testable without a
      // real request or real credentials.
      static List<String> allTrades(Signer signer, BiFunction<Signer, String, Page> fetchPageFn) {
          List<String> ids = new ArrayList<>();
          String cursor = null;
          while (true) {
              Page page = fetchPageFn.apply(signer, cursor);
              ids.addAll(page.tradeIds);
              if (!page.hasNext) return ids;
              cursor = page.nextCursor; // opaque token, round-trip verbatim
          }
      }
      // Adapts fetchPage (which throws) to the checked-exception-free
      // BiFunction shape allTrades needs.
      static Page fetchPageUnchecked(Signer signer, String cursor) {
          try {
              return fetchPage(signer, cursor);
          } catch (Exception e) {
              throw new RuntimeException(e);
          }
      }
      public static void main(String[] args) {
          // SignRequest is the real helper from /v3/authentication, compiled
          // on the classpath alongside this file — see that page's example.
          List<String> ids = allTrades(SignRequest::signRequest, AllTrades::fetchPageUnchecked);
          System.out.println(ids);
      }
  }
  ```

  ```cpp C++ theme={null}
  // Walks GET /v3/fx/trades forward (newest -> oldest) until hasNext is
  // false — the pattern from
  // /v3/pagination#worked-example-fetch-all-trades-forward-iteration.
  // Requires OpenSSL and libcurl.
  #include <curl/curl.h>
  #include <functional>
  #include <optional>
  #include <string>
  #include <vector>
  // Matches the definition in ../../authentication/cpp/sign_request.cpp,
  // compiled together with this file — see that file for the real
  // implementation of signRequest.
  struct SignedRequest {
      std::string authHeader;
      std::string sigHeader;
  };
  SignedRequest signRequest(const std::string& method, const std::string& urlPath,
                             const std::string& query, const std::string& body);
  // A minimal page shape: just the two fields the walking logic needs.
  struct Page {
      std::vector<std::string> tradeIds;
      bool hasNext;
      std::optional<std::string> nextCursor;
  };
  using Signer = std::function<SignedRequest(const std::string&, const std::string&, const std::string&, const std::string&)>;
  using FetchPageFn = std::function<Page(const Signer&, const std::optional<std::string>&)>;
  static size_t discard(char*, size_t s, size_t n, void*) { return s * n; }
  // Fetches one page. Takes signer as a parameter so the cursor-walking
  // logic below is testable without a real request.
  Page fetchPage(const Signer& signer, const std::optional<std::string>& cursor) {
      std::string query = "limit=100";
      if (cursor) query += "&startingAfter=" + *cursor;
      std::string url = "https://api.openfx.com/v3/fx/trades?" + query;
      // Sign per page: the wall-clock to walk all pages is unbounded by the
      // JWT's 60s TTL.
      SignedRequest req = signer("GET", "/v3/fx/trades", query, "");
      CURL* curl = curl_easy_init();
      curl_slist* headers = nullptr;
      headers = curl_slist_append(headers, ("Authorization: " + req.authHeader).c_str());
      headers = curl_slist_append(headers, ("X-Request-Signature: " + req.sigHeader).c_str());
      curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard);
      curl_easy_perform(curl);
      curl_slist_free_all(headers);
      curl_easy_cleanup(curl);
      // A real implementation would parse the JSON response body here,
      // using whatever JSON library you're already using.
      return {{}, false, std::nullopt};
  }
  // Walks forward until hasNext is false. Takes fetchPage as a parameter so
  // the cursor-threading and termination logic is testable without a real
  // request or real credentials.
  std::vector<std::string> allTrades(const Signer& signer, const FetchPageFn& fetchPageFn) {
      std::vector<std::string> ids;
      std::optional<std::string> cursor = std::nullopt;
      while (true) {
          Page page = fetchPageFn(signer, cursor);
          for (auto& id : page.tradeIds) ids.push_back(id);
          if (!page.hasNext) return ids;
          cursor = page.nextCursor; // opaque token, round-trip verbatim
      }
  }
  #ifndef ALL_TRADES_NO_MAIN
  #include <iostream>
  int main() {
      try {
          auto ids = allTrades(signRequest, fetchPage);
          for (auto& id : ids) std::cout << id << std::endl;
          return 0;
      } catch (const std::exception& e) {
          std::cerr << "Error: " << e.what() << std::endl;
          return 1;
      }
  }
  #endif
  ```

  ```go Go theme={null}
  // Walks GET /v3/fx/trades forward (newest -> oldest) until hasNext is
  // false — the pattern from
  // /v3/pagination#worked-example-fetch-all-trades-forward-iteration.
  package main
  import (
  	"fmt"
  	"net/http"
  	// auth is the real signRequest helper from /v3/authentication — see
  	// go.mod's replace directive if you've copied it somewhere else.
  	auth "openfx-v3-authentication-example"
  )
  // Matches auth.SignRequest's signature, so that function can be passed
  // directly wherever a SignerFunc is expected.
  type SignerFunc func(method, url string, body []byte) (*auth.SignedRequest, error)
  // A minimal page shape: just the two fields the walking logic needs.
  type Page struct {
  	TradeIDs   []string
  	HasNext    bool
  	NextCursor *string
  }
  // Fetches one page. Takes signer as a parameter so the cursor-walking
  // logic below is testable without a real request.
  func fetchPage(signer SignerFunc, cursor *string) (*Page, error) {
  	url := "https://api.openfx.com/v3/fx/trades?limit=100"
  	if cursor != nil {
  		url += "&startingAfter=" + *cursor
  	}
  	// Sign per page: the wall-clock to walk all pages is unbounded by the
  	// JWT's 60s TTL.
  	signed, err := signer("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	req, err := http.NewRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	for k, v := range signed.Headers {
  		req.Header.Set(k, v)
  	}
  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		return nil, err
  	}
  	defer res.Body.Close()
  	// A real implementation would parse the JSON response body here.
  	return &Page{HasNext: false}, nil
  }
  type fetchPageFunc func(signer SignerFunc, cursor *string) (*Page, error)
  // Walks forward until hasNext is false. Takes fetchPage as a parameter so
  // the cursor-threading and termination logic is testable without a real
  // request or real credentials.
  func allTrades(signer SignerFunc, fetchPageFn fetchPageFunc) ([]string, error) {
  	var ids []string
  	var cursor *string
  	for {
  		page, err := fetchPageFn(signer, cursor)
  		if err != nil {
  			return nil, err
  		}
  		ids = append(ids, page.TradeIDs...)
  		if !page.HasNext {
  			return ids, nil
  		}
  		cursor = page.NextCursor // opaque token, round-trip verbatim
  	}
  }
  func main() {
  	ids, err := allTrades(auth.SignRequest, fetchPage)
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println(ids)
  }
  ```

  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # Walks GET /v3/fx/trades forward (newest -> oldest) until hasNext is false
  # — the pattern from
  # /v3/pagination#worked-example-fetch-all-trades-forward-iteration.
  # Illustrative only: cURL can't compute the ES256 request signature itself,
  # so $OPENFX_JWT/$OPENFX_SIGNATURE below are placeholders — get real values
  # from signRequest, in any supported language, via /v3/authentication. Sign
  # per page: the wall-clock to walk all pages is unbounded by the JWT's 60s
  # TTL.
  set -euo pipefail
  extract_field() { # $1: JSON, $2: field name
    echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed -E "s/.*:\"([^\"]*)\"/\1/"
  }
  extract_bool() { # $1: JSON, $2: field name
    echo "$1" | grep -o "\"$2\":[a-z]*" | head -1 | sed -E "s/.*:([a-z]*)/\1/"
  }
  CURSOR=""
  while true; do
    URL="https://api.openfx.com/v3/fx/trades?limit=100"
    if [ -n "$CURSOR" ]; then
      URL="$URL&startingAfter=$CURSOR"
    fi
    export OPENFX_JWT="<jwt-from-signRequest>"
    export OPENFX_SIGNATURE="<signature-from-signRequest>"
    PAGE=$(curl -s "$URL" \
      -H "Authorization: Bearer $OPENFX_JWT" \
      -H "X-Request-Signature: $OPENFX_SIGNATURE")
    echo "$PAGE" | grep -o '"id":"[^"]*"'
    HAS_NEXT=$(extract_bool "$PAGE" "hasNext")
    if [ "$HAS_NEXT" != "true" ]; then
      break
    fi
    CURSOR=$(extract_field "$PAGE" "nextCursor") # opaque token, round-trip verbatim
  done
  ```
</CodeGroup>

## Worked example: walk backward from a known cursor

The mirror image of the forward walk: page from a starting point toward the newest record, stopping when `hasPrev` is `false` (equivalently, when `prevCursor` is `null`). Useful for catch-up loops that recover a gap older than your most recent sync.

```javascript theme={null}
// `signRequest({ method, url, body })` returns { headers, body } with a freshly minted JWT
// AND a freshly computed X-Request-Signature each call. See the Authentication guide.
// `startCursor` is a cursor you saved from a previous request. Iteration ends when
// prevCursor === null (the head of the list).
async function* tradesNewerThan(signRequest, startCursor) {
  let cursor = startCursor;
  while (cursor !== null) {
    const url = new URL("https://api.openfx.com/v3/fx/trades");
    url.searchParams.set("limit", "100");
    url.searchParams.set("endingBefore", cursor);

    const signed = signRequest({
      method: "GET",
      url: url.toString(),
    });
    const page = await fetch(url, { headers: signed.headers }).then((r) =>
      r.json(),
    );

    for (const trade of page.data) yield trade;
    cursor = page.pagination.prevCursor; // opaque; null when we've reached the newest record
  }
}
```

## Worked example: fetch "what's new since last seen"

The typical sync-job pattern: you've persisted the most recent cursor from your last poll; on this poll you want only the new records that have appeared since.

```javascript theme={null}
// `signRequest({ method, url, body })` returns { headers, body } with a freshly minted JWT
// AND a freshly computed X-Request-Signature each call. See the Authentication guide.
// `lastSeenCursor` is the most recent prevCursor (or, for the very first call,
// the nextCursor from the most recent record you saved). If this is your first run,
// do a one-time "load all" instead, then save the newest cursor from page 1's response.
//
// One-shot per poll cycle: signing once here is fine because there's a single HTTP call.
// If you wrap this in a sync loop that runs every N minutes, sign inside the loop body,
// not outside it. See Rate limiting → Handling rate limits pattern:
// https://docs.openfx.com/v3/rate-limiting#handling-rate-limits-pattern
const url = new URL("https://api.openfx.com/v3/fx/trades");
url.searchParams.set("limit", "25");
url.searchParams.set("endingBefore", lastSeenCursor);

const signed = signRequest({ method: "GET", url: url.toString() });
const page = await fetch(url, { headers: signed.headers }).then((r) =>
  r.json(),
);

// page.data contains records newer than the cursor, newest-first within the page.
// Persist page.pagination.prevCursor so the next poll picks up where this one ended.
const newLastSeenCursor = page.pagination.prevCursor ?? lastSeenCursor;
```

<Warning>
  For a continuous sync loop, persist `newLastSeenCursor` **only after you've
  successfully handled the records in `page.data`**. If your handler crashes
  between the API call and the DB write, the next poll re-fetches the same
  records. That's safe, because your handler should be idempotent on resource
  ID.
</Warning>

## Why cursor pagination

Page-based pagination (`?page=2&limit=25`) breaks down at scale:

* New records appearing during pagination cause records to shift between pages (you see the same trade twice or miss one)
* Deep pages get progressively slower (the database has to skip every prior row)
* "How many pages total?" requires a separate `COUNT(*)` query

Cursors fix all three. The cursor encodes the row's position; the server seeks directly to that position and reads the next page from there. The `pagination` envelope makes the cursor a first-class field of the response, so clients never have to guess where the cursor "is" — the server hands it back to you.

## Common mistakes

* **Decoding or parsing the cursor.** Cursors are opaque. Don't decode them, don't try to extract anything from them, don't munge them. Pass the string back exactly as you received it. The format is an implementation detail and will change.
* **Picking the wrong cursor for the direction you want.** For "load all" (forward), use `pagination.nextCursor` and pass it as `?startingAfter=`. For "what's new since" (backward), use `pagination.prevCursor` and pass it as `?endingBefore=`. Mixing them sends you in the wrong direction.
* **Sorting the `data` array client-side and then deriving a cursor.** The cursor is in the response envelope, not in the array. Read `pagination.nextCursor` / `pagination.prevCursor` directly; sort `data` for display second.
* **Re-deriving "is there more?" from the cursor when the boolean is right there.** `pagination.hasNext` and `pagination.hasPrev` are the canonical end-of-list signals — branch on them. (`nextCursor` / `prevCursor` flip to `null` in lockstep, so a null-check works too, but the booleans read clearer.)
* **Setting both `startingAfter` and `endingBefore`.** They are mutually exclusive. Pick a direction.
* **Treating `limit` as a guarantee.** It's a max. The server may return fewer records. Paginate until `pagination.hasNext` is `false`, not until `data.length < limit`.
* **Persisting cursors longer than the resource lives.** Cursors point to a position in the underlying sort. If the record they point to is purged (rare on Trade / Trade-account resources at any meaningful timescale), paginating past it returns the next-newest record.

## Gotchas

* **Cursors are stable across requests.** A cursor you persisted yesterday continues to point to the same position in the list today (subject to record retention).
* **`limit` is a max, not a guarantee.** The server may return fewer records than requested if filters apply.
* **`nextCursor` and `prevCursor` reflect the chosen direction independently.** A page returned via `?startingAfter=` still carries a `prevCursor` you can use to walk back if you need to. The envelope is symmetric — only the request parameter says which direction you're moving.
* **Empty page on the boundary.** Calling `?startingAfter=<cursor at oldest record>` returns `data: []` with `pagination.hasNext: false` and `pagination.nextCursor: null` — no error.

## What's next

<CardGroup cols={2}>
  <Card title="Rate limiting" icon="gauge-high" href="/v3/rate-limiting#handling-rate-limits-pattern">
    The back-off pattern for back-fill loops that hit `RATE_LIMIT_EXCEEDED`.
  </Card>

  <Card title="Idempotency" icon="repeat" href="/v3/idempotency">
    Make sync jobs safe across crashes and restarts.
  </Card>

  <Card title="List trades" icon="list" href="/v3/api-reference/trade/list-trades">
    Paginate trades via the canonical list endpoint.
  </Card>

  <Card title="Retrieve a trade" icon="magnifying-glass" href="/v3/api-reference/trade/get-trade">
    Fetch a single record by ID without paging.
  </Card>
</CardGroup>
