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.
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 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. 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.Get a starting cursor
A first run has no saved cursor, and the newest page returnsprevCursor: 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).
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.
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. (A cursor isnullwhenever its boolean isfalse, but the booleans are the contract.) - 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, 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.