GET /v3/fx/trades, GET /v3/fx/deposits, GET /v3/fx/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
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.
Response envelope
Paginated responses wrap the array of records in apagination 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, 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.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. Supposepagination.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) untilhasNext 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 whenhasPrev 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.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
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.nextCursorand pass it as?startingAfter=. For “what’s new since” (backward), usepagination.prevCursorand pass it as?endingBefore=. Mixing them sends you in the wrong direction. - Sorting the
dataarray client-side and then deriving a cursor. The cursor is in the response envelope, not in the array. Readpagination.nextCursor/pagination.prevCursordirectly; sortdatafor display second. - Re-deriving “is there more?” from the cursor when the boolean is right there.
pagination.hasNextandpagination.hasPrevare the canonical end-of-list signals — branch on them. (nextCursor/prevCursorflip tonullin lockstep, so a null-check works too, but the booleans read clearer.) - Setting both
startingAfterandendingBefore. They are mutually exclusive. Pick a direction. - Treating
limitas a guarantee. It’s a max. The server may return fewer records. Paginate untilpagination.hasNextisfalse, not untildata.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).
limitis a max, not a guarantee. The server may return fewer records than requested if filters apply.nextCursorandprevCursorreflect the chosen direction independently. A page returned via?startingAfter=still carries aprevCursoryou 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>returnsdata: []withpagination.hasNext: falseandpagination.nextCursor: null— no 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.