Skip to main content
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, and pass them back unchanged. When it matters. Any time you list trades, deposits, withdrawals, withdrawal accounts, accounts, payout addresses, or payouts. 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. These v3 list endpoints use cursor pagination: GET /v3/fx/trades, GET /v3/fx/deposits, GET /v3/fx/withdrawals, GET /v3/fx/withdrawal-accounts, GET /v3/common/accounts, GET /v3/payments/payout-addresses, and GET /v3/payments/payouts. 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

startingAfter and endingBefore are mutually exclusive. Set one or the other, not both. On the first request, omit both. You get the newest page.
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.
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.

Response envelope

Paginated responses wrap the array of records in a pagination envelope that carries the cursors for navigation:
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.
Bounded lists do not paginate. GET /v3/fx/pairs and GET /v3/fx/balances return their complete data array without cursor parameters or a pagination object. Apply the cursor flow on this page to the paginated lists above.
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.

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: 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.

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.

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.

Get a starting cursor

A first run has no saved cursor, and the newest page returns prevCursor: null, so there is nothing to start from. Request a single-record page instead and keep its nextCursor. A nextCursor sits on the last record of its page, so with limit=1 it sits on the newest record, and endingBefore with that cursor returns only records newer than it. The nextCursor is null when the list holds one record or none (hasNext is false), so wait and request again until it returns a cursor. You use the same request on every later poll to refresh your saved cursor (see Poll for new records).
Take this cursor before you load existing history. Records created while the load runs then arrive in your first poll instead of falling between the two. Because a poll can return a record more than once, make your handler idempotent on resource ID.
The starting-cursor request applies to the FX lists (GET /v3/fx/trades, /deposits, /withdrawals, and /withdrawal-accounts).

Poll for new records

A backward page (endingBefore) returns prevCursor only while more newer records remain (hasPrev is true). The page that reaches the newest record returns prevCursor: null, so it gives you nothing to save. To move your saved cursor forward, request the newest-record cursor again with the limit=1 request above. Request that cursor before you read the new records, so a record created during the poll arrives in the next poll instead of falling between the two. Then page toward the newest record with prevCursor until hasPrev is false, and save the refreshed cursor.
For a continuous sync loop, persist the returned cursor only after you’ve successfully handled the records in every page. If your handler crashes before the DB write, the next poll re-fetches the same records. That’s safe, because your handler should be idempotent on resource ID.

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. (A cursor is null whenever its boolean is false, but the booleans are the contract.)
  • 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, not an error.

What’s next

Rate limiting

The back-off pattern for back-fill loops that hit RATE_LIMIT_EXCEEDED.

Idempotency

Make sync jobs safe across crashes and restarts.

List trades

Paginate trades via the canonical list endpoint.

Retrieve a trade

Fetch a single record by ID without paging.