Batching requests
The single most effective optimization on oneapi.finance is batched requests.
A /v1/quote?symbols=A,B,C,D,E,F,G,H call costs one token and returns
eight quotes. The naive equivalent (eight separate calls) costs eight tokens.
This recipe covers the endpoints that support batching, the right batch size, and parallelism patterns.
Endpoints that support batching
| Endpoint | Batch param | Max per call |
|---|---|---|
/v1/quote | symbols=A,B,C | 8 |
/v1/dividends | symbols=A,B,C | 8 |
/v1/fx/time_series | pairs=USD/EUR,GBP/JPY | 10 |
Endpoints that do not batch (/v1/time_series, /v1/statistics,
/v1/profile, /v1/splits, /v1/symbol_search) take a single symbol per
call and you should parallelize them client-side.
Why eight
Internally, batches are decomposed and dispatched to upstream sources in parallel. The cache layer is keyed per-symbol, so an 8-symbol batch with 7 cache hits and 1 miss only ever fetches one symbol from upstream. The 8-symbol ceiling exists because:
- Beyond ~8, the marginal benefit of avoiding HTTP overhead falls off relative to the cost of holding one HTTP connection open longer.
- Fan-out concurrency on the upstream side is bounded by source rate limits; beyond 8, we would be queuing inside our gateway, which is just hiding latency from you.
For 9+ symbols, parallelize 8-symbol batches client-side.
Batch + parallel fan-out
import httpximport asyncio
API_KEY = "oa_live_..."HEADERS = {"Authorization": f"Bearer {API_KEY}"}CHUNK = 8MAX_CONCURRENCY = 4 # 4 batches × 8 symbols = 32 symbols/wave
def chunked(it, n): out = [] for x in it: out.append(x) if len(out) == n: yield out out = [] if out: yield out
async def quotes_async(symbols: list[str]) -> dict[str, dict]: sem = asyncio.Semaphore(MAX_CONCURRENCY) async with httpx.AsyncClient(headers=HEADERS, timeout=15.0) as client:
async def one_batch(batch: list[str]) -> dict: async with sem: r = await client.get( "https://api.oneapi.finance/v1/quote", params={"symbols": ",".join(batch)}, ) r.raise_for_status() return r.json().get("quotes", {})
results = await asyncio.gather(*[one_batch(b) for b in chunked(symbols, CHUNK)])
out = {} for d in results: out.update(d) return out
# 100 symbols → ceil(100/8) = 13 batched calls, max 4 in flight.The same logic in JS:
const CHUNK = 8;const MAX_CONCURRENCY = 4;
async function quotes(symbols) { const batches = []; for (let i = 0; i < symbols.length; i += CHUNK) { batches.push(symbols.slice(i, i + CHUNK)); } const out = {}; for (let i = 0; i < batches.length; i += MAX_CONCURRENCY) { const wave = batches.slice(i, i + MAX_CONCURRENCY); const results = await Promise.all( wave.map(async (batch) => { const url = `https://api.oneapi.finance/v1/quote?symbols=${batch.join(",")}`; const r = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ONEAPI_KEY}` } }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const body = await r.json(); return body.quotes ?? {}; }), ); for (const d of results) Object.assign(out, d); } return out;}Quota math
For 100 symbols hit every minute on the Indie tier:
| Strategy | Calls per minute | Calls per day | Days to exhaust 100k quota |
|---|---|---|---|
| Single, sequential | 100 | 144,000 | 0.7 |
| Single, parallel (10 concurrent) | 100 | 144,000 | 0.7 |
| Batched (8/call), sequential | 13 | 18,720 | ~5.3 |
| Batched (8/call), parallel (4 concurrent) | 13 | 18,720 | ~5.3 |
| Batched + cached at 60s TTL on the client | 1-13 (depending on hit rate) | under 18,720 | many |
The combination of batching and caching is what makes the Indie tier viable for non-trivial dashboards.
Handling per-symbol failures
For batched /v1/quote and /v1/dividends, individual symbol errors are
not propagated as a non-2xx status. Instead, the failed symbols are listed
in data_issues:
{ "quotes": { "AAPL": { "...": "..." } }, "data_issues": ["XYZQ: not_found"]}Always check data_issues so you can mark those symbols invalid in your UI
and stop requesting them.
When not to batch
- When you need a different parameter per call (e.g. different
outputsizeorinterval). Time-series queries are single-symbol for this reason. - When latency for the first symbol matters more than total throughput. A batched request waits for the slowest upstream in the batch before responding.
See also
- Rate limits
- Caching recipe
/v1/quote— primary batched endpoint.