TPP · Banking · Bank Data Sharing

Pagination 3 min read

List endpoints on the Bank Data Sharing API return pagination information in the response body. The TPP walks through a paginated result set by following the URLs in the Links object — there are no page or page-size query parameters to set on the request.

For the end-to-end picture — including how the API Hub converts LFI meta into the TPP Links envelope — see Pagination — LFI meta to TPP Links.

01 Two response shapes to handle

Paginated and unpaginated, one loop

Pagination is ultimately driven by the LFI. The UAE Open Finance standard requires LFIs to paginate /accounts/{AccountId}/transactions and /accounts/{AccountId}/statements, but during rollout not every LFI will have implemented it on day one. A TPP SHOULD therefore be ready for either of the following:

  • Paginated — the LFI returned a page of the filtered result set. Meta.TotalPages > 1 and Links.Next is populated while more pages remain.
  • Unpaginated — the LFI returned the full result set in a single response. Meta.TotalPages is 1 (or 0 for an empty result) and Links contains only Self.

The loop pattern below — follow Links.Next until it is absent — handles both cases without branching. A TPP that only supports the paginated shape will appear to work correctly while the LFI is unpaginated (it fetches page 1 and stops), but will silently truncate to the first page once the LFI enables pagination. Using Links.Next as the stop condition avoids that regression.

02 Which endpoints paginate

Required vs optional pagination

EndpointPagination
GET/accounts/{AccountId}/transactionsRequired
GET/accounts/{AccountId}/statementsRequired
GET/accountsOptional
GET/accounts/{AccountId}/balancesOptional
GET/accounts/{AccountId}/beneficiariesOptional
GET/accounts/{AccountId}/direct-debitsOptional
GET/accounts/{AccountId}/scheduled-paymentsOptional
GET/accounts/{AccountId}/standing-ordersOptional
GET/accounts/{AccountId}/productsOptional
GET/accounts/{AccountId}/partiesOptional

Transactions and statements span long history (at least two years) and routinely produce large result sets. For other list endpoints the full result set is usually returned in one response, but TPPs SHOULD still follow Links.Next defensively in case an LFI chooses to paginate them.

04 The Meta object

Counts and history boundaries

FieldApplies toMeaning
TotalPagesAll list endpointsTotal number of pages in the filtered result set. 1 when the response is unpaginated, 0 for an empty result set
FirstAvailableDateTimeTransactions, statementsISO 8601 timestamp of the earliest record the LFI holds for this account
LastAvailableDateTimeTransactions, statementsISO 8601 timestamp of the most recent record the LFI holds for this account

FirstAvailableDateTime and LastAvailableDateTime reflect the full history the LFI holds, not the slice returned by the current query. They are useful for narrowing follow-up requests with fromBookingDateTime / toBookingDateTime.

05 Following Links.Next

The canonical fetch loop

The canonical pattern is a loop that fetches Links.Next until the field is absent. The same logic terminates correctly whether the LFI paginated the result or returned it in a single response.

typescript
import crypto from 'node:crypto'

async function fetchAllTransactions(accountId: string, fromBookingDateTime: string) {
  const transactions: unknown[] = []

  // Initial request — no page parameters, the Hub paginates via Links
  let nextUrl: string | undefined =
    `${LFI_API_BASE}/open-finance/v2.1/accounts/${accountId}/transactions` +
    `?fromBookingDateTime=${encodeURIComponent(fromBookingDateTime)}`

  while (nextUrl) {
    const response = await fetch(nextUrl, {
      headers: {
        Authorization:                `Bearer ${access_token}`,
        'x-fapi-interaction-id':      crypto.randomUUID(),
        'x-fapi-auth-date':           lastCustomerAuthDate,
        'x-fapi-customer-ip-address': customerIpAddress,
      },
      // agent: new https.Agent({ cert: transportCert, key: transportKey }),
    })

    const body = await response.json()
    transactions.push(...body.Data.Transaction)

    // Stop when the server does not return a Next link — works for both
    // paginated (last page reached) and unpaginated (single response) cases
    nextUrl = body.Links.Next
  }

  return transactions
}
Reuse the access token across pages

The access token is valid for the same 10-minute window used elsewhere. For very large result sets, refresh proactively using the refresh token flow in Step 10 — Refreshing the Access Token rather than waiting for a 401 mid-loop.

Rate limits and bounded retries

Each page is a separate HTTP request. Cap your loop with a sensible maximum page count and respect any 429 Too Many Requests backoff the API Hub returns — do not retry Links.Next in a tight loop.

06 API Sequence Flow

Walking pages end-to-end

The diagram below traces a three-page transactions query through the API Hub. The TPP makes a single unparameterised request and then follows Links.Next on each response; the Hub translates each call into the corresponding page / page-size request to the LFI and converts the LFI's meta back into the Links envelope.

Sequence diagramPagination API FlowClick to expand
07 Worked example — transactions, 3-page result

Walking three pages of transactions

Initial request:

initial requesthttp
GET /accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00:00:00Z

Response — page 1 of 3:

page 1 responsejson
{
  "Data": {
    "AccountId": "acc-001",
    "Transaction": [ /* ... 100 items ... */ ]
  },
  "Links": {
    "Self":  "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=1",
    "First": "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=1",
    "Next":  "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=2",
    "Last":  "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=3"
  },
  "Meta": {
    "TotalPages":             3,
    "FirstAvailableDateTime": "2022-03-14T08:21:00+00:00",
    "LastAvailableDateTime":  "2026-04-18T11:47:00+00:00"
  }
}

The TPP follows Links.Next. On page 3, the response has no Next field:

page 3 responsejson
{
  "Data": { "AccountId": "acc-001", "Transaction": [ /* ... last slice ... */ ] },
  "Links": {
    "Self":  "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=3",
    "First": "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=1",
    "Prev":  "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=2",
    "Last":  "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z&page=3"
  },
  "Meta": {
    "TotalPages":             3,
    "FirstAvailableDateTime": "2022-03-14T08:21:00+00:00",
    "LastAvailableDateTime":  "2026-04-18T11:47:00+00:00"
  }
}

The loop terminates on the absence of Links.Next.

08 Worked example — LFI returns everything in one response

Unpaginated response handled by the same loop

When the LFI has not enabled pagination, the same request returns the full result set in one response:

unpaginated responsejson
{
  "Data": {
    "AccountId": "acc-001",
    "Transaction": [ /* ... every matching transaction ... */ ]
  },
  "Links": {
    "Self": "https://rs1.lfi.apihub.openfinance.ae/open-finance/v2.1/accounts/acc-001/transactions?fromBookingDateTime=2026-01-01T00%3A00%3A00Z"
  },
  "Meta": {
    "TotalPages":             1,
    "FirstAvailableDateTime": "2022-03-14T08:21:00+00:00",
    "LastAvailableDateTime":  "2026-04-18T11:47:00+00:00"
  }
}

Links.Next is absent, so the loop exits after the first iteration. No TPP-side branching is required.

09 Empty result sets

No matches is a 200, not a 404

A query that matches no records returns 200 with an empty Data array, Meta.TotalPages: 0, and Links containing only Self. Do not treat this as an error — 404 is not returned for empty filtered results.