Skip to main content

Pagination

Many Alpaca list endpoints return results in pages keyed by a next_page_token (or page_token). The SDK manages those tokens for you at two levels.

Built-in iterate / collect helpers

Most paginated endpoints have a ready-made pair on the facade: an iterate* helper that lazily yields across pages, and a collect* helper that eagerly returns everything. The page token is handled internally.

// Lazily yield account activities across all pages.
for await (const activity of alpaca.trading.iterateActivities({ activityTypes: ["FILL"] })) {
console.log(activity.id);
}

// Eagerly collect option contracts into one array.
const contracts = await alpaca.trading.collectOptionsContracts({ underlyingSymbols: "AAPL" });

The common historical bar, trade, quote, and auction helpers yield { symbol, value } records. Their collect*BySymbol variants merge pages into symbol-keyed arrays:

import { TimeFrame } from "@alpacahq/alpaca-trade-api";

// Stream `{ symbol, value }` records as they arrive.
for await (const { symbol, value } of alpaca.marketData.iterateStockBars({
symbols: ["AAPL"],
timeframe: TimeFrame.Day,
start: new Date("2024-01-01"),
})) {
console.log(symbol, value.c);
}

// Or collect everything, keyed by symbol.
const bySymbol = await alpaca.marketData.collectStockBarsBySymbol({
symbols: ["AAPL", "MSFT"],
timeframe: TimeFrame.Day,
start: new Date("2024-01-01"),
});

The exact collected shape follows the endpoint:

  • Symbol-keyed arrays cover stock bars/trades/quotes/auctions, crypto bars/trades/quotes, option bars/trades, index values, and forex rates.
  • Symbol-keyed objects cover option snapshots and option chains, where each symbol has one snapshot rather than an array.
  • Top-level arrays cover news and the single-symbol stock bar/trade/quote/auction endpoints.
  • Corporate actions use a corporate-action envelope whose typed action arrays are merged across pages.

Every token- and cursor-based helper tracks the full traversal history. If an endpoint returns any previously visited token/cursor — including a longer cycle such as A → B → A — iteration stops instead of fetching it again. Items and corporate-action pages already fetched remain yielded/collected; only the revisited request is suppressed.

The generic pagination helper

For any endpoint without a dedicated helper, the exported pagination helper turns a token-taking call into a single async iterator:

import { pagination } from "@alpacahq/alpaca-trade-api";

for await (const trade of pagination.paginate(async (pageToken) => {
const response = await alpaca.marketData.stocks.stockTrades({
symbols: "AAPL",
pageToken,
});
return {
items: response.trades?.AAPL ?? [],
nextPageToken: response.nextPageToken,
};
})) {
console.log(trade);
}

In every case the next page is requested only as you consume the current one, so large histories stream lazily instead of buffering everything in memory. The same full-history cycle guard applies to these generic helpers.

Cursor-based endpoints use pagination.paginateCursor with a cursor extracted from the last item. It provides the same lazy backpressure: it fetches the next page only after the current page has been consumed.

import { pagination, trading } from "@alpacahq/alpaca-trade-api";

const cursorOptions: pagination.CursorOptions<
trading.GetAccountActivities200ResponseInner
> = {
fetchPage: (pageToken) =>
alpaca.trading.accountActivities.getAccountActivities({
pageToken,
pageSize: 100,
}),
getCursor: (activity) => activity.id,
pageSize: 100,
};

for await (const activity of pagination.paginateCursor(cursorOptions)) {
console.log(activity.id);
}

Bounding large fetches

Eager collection can consume substantial memory. Multi-symbol collect*BySymbol methods and normalized get* accessors accept SymbolCollectOptions:

// Stop after at most 1,000 records for each requested symbol.
const recent = await alpaca.marketData.getStockBars(
{
symbols: ["AAPL", "MSFT"],
timeframe: TimeFrame.Minute,
start,
},
{ maxPerSymbol: 1_000 },
);

By default, every symbol is multiplexed into one request and its page-token chain is followed sequentially. The generic pagination.collect and pagination.collectCursor accept maxItems; pagination.collectBySymbol accepts maxPerSymbol. Supply the expected symbols to collectBySymbol so it can stop fetching as soon as every requested symbol reaches that bound:

import {
marketData,
pagination,
TimeFrame,
trading,
} from "@alpacahq/alpaca-trade-api";

const cursorOptions: pagination.CursorOptions<
trading.GetAccountActivities200ResponseInner
> = {
fetchPage: (pageToken) =>
alpaca.trading.accountActivities.getAccountActivities({
pageToken,
pageSize: 100,
}),
getCursor: (activity) => activity.id,
pageSize: 100,
};

const activities = await pagination.collectCursor(cursorOptions, {
maxItems: 500,
});

const fetchBarsPage: pagination.SymbolMapPageFetcher<marketData.StockBar> =
async (pageToken) => {
const response = await alpaca.marketData.stocks.stockBars({
symbols: "AAPL,MSFT",
timeframe: TimeFrame.Day,
start: new Date("2024-01-01"),
pageToken,
});
return {
data: response.bars ?? {},
nextPageToken: response.nextPageToken,
};
};

const bars = await pagination.collectBySymbol(fetchBarsPage, {
symbols: ["AAPL", "MSFT"],
maxPerSymbol: 1_000,
});

collectCursor and collectBySymbol are eager collectors, so set these bounds when the full history might not fit comfortably in memory. Without expected symbols, collectBySymbol still caps each array but must follow the token chain to completion because a new symbol could appear on a later page.

Controlled fan-out

For a large basket over a long range, split symbols into chunks and cap the number of in-flight requests:

const basket = await alpaca.marketData.getStockBars(
{
symbols: bigList,
timeframe: TimeFrame.Day,
start,
},
{
concurrency: 4,
chunkSize: 1,
maxPerSymbol: 5_000,
},
);

concurrency defaults to 1, preserving the single combined request. chunkSize defaults to 1 and is used when concurrency > 1; it controls how many symbols share each parallel request. The client's rate limiter still bounds the actual request rate.

For custom fan-out, pagination.chunk(items, size) creates consecutive groups, and pagination.mapConcurrent(items, concurrency, worker) preserves input order while keeping at most the requested number of workers in flight. Its first rejection rejects the whole operation.

import {
Alpaca,
pagination,
TimeFrame,
} from "@alpacahq/alpaca-trade-api";

const alpaca = new Alpaca({
keyId: process.env.APCA_API_KEY_ID,
secret: process.env.APCA_API_SECRET_KEY,
});
const bigList = ["AAPL", "MSFT", "GOOG", "AMZN"];
const start = new Date("2024-01-01");
const groups = pagination.chunk(bigList, 25);
const results = await pagination.mapConcurrent(groups, 4, async (symbols) => {
return alpaca.marketData.getStockBars({
symbols,
timeframe: TimeFrame.Day,
start,
});
});