@lunch-money/developer-docs 2.11.0-preview.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,517 @@
1
+ # Pagination
2
+
3
+ The Lunch Money API uses offset-based pagination for the `GET /transactions` endpoint to efficiently retrieve large sets of transaction data. This guide explains how pagination works and how to implement it in your application.
4
+
5
+ ## Overview
6
+
7
+ When retrieving transactions using `GET /transactions`, the API supports pagination through the `limit` and `offset` query parameters. By default, the endpoint returns up to 1000 transactions per request. If more transactions match your filter criteria, the response includes a `has_more` property set to `true`, indicating that additional pages are available.
8
+
9
+ > [!NOTE] Pagination Scope
10
+ > Pagination works with all filter parameters including date ranges, account IDs, categories, tags, and status filters. The pagination applies to the filtered result set.
11
+
12
+ ## How Pagination Works
13
+
14
+ ### Parameters
15
+
16
+ - **`limit`** (optional): The maximum number of transactions to return in a single request
17
+ - Default: `1000`
18
+ - Minimum: `1`
19
+ - Maximum: `2000`
20
+ - If omitted, defaults to 1000 transactions
21
+
22
+ - **`offset`** (optional): The number of transactions to skip before returning results
23
+ - Default: `0` (start from the beginning)
24
+ - Minimum: `0`
25
+ - Use this to fetch subsequent pages of results
26
+
27
+ ### Response Property
28
+
29
+ - **`has_more`**: A boolean property in the response indicating whether more transactions are available
30
+ - `true`: More transactions match your filter criteria and are available on subsequent pages
31
+ - `false`: All matching transactions have been returned
32
+
33
+ ### Basic Pagination Flow
34
+
35
+ 1. Make your first request (optionally with `limit` specified, defaults to 1000)
36
+ 2. Check the `has_more` property in the response
37
+ 3. If `has_more` is `true`, make another request with `offset` set to your previous `offset + limit`
38
+ 4. Repeat until `has_more` is `false`
39
+
40
+ ## Examples
41
+
42
+ ### Basic Pagination
43
+
44
+ Here's how to fetch transactions page by page:
45
+
46
+ :::tabs
47
+ @tab JavaScript/Node.js
48
+ ```javascript
49
+ async function getAllTransactions(accessToken, limit = 1000) {
50
+ const allTransactions = [];
51
+ let offset = 0;
52
+ let hasMore = true;
53
+
54
+ while (hasMore) {
55
+ const url = `https://api.lunchmoney.dev/v2/transactions?limit=${limit}&offset=${offset}`;
56
+ const response = await fetch(url, {
57
+ headers: {
58
+ 'Authorization': `Bearer ${accessToken}`
59
+ }
60
+ });
61
+
62
+ const data = await response.json();
63
+ allTransactions.push(...data.transactions);
64
+
65
+ hasMore = data.has_more;
66
+ offset += limit;
67
+
68
+ console.log(`Fetched ${data.transactions.length} transactions (total: ${allTransactions.length})`);
69
+ }
70
+
71
+ return allTransactions;
72
+ }
73
+
74
+ // Usage
75
+ const transactions = await getAllTransactions('YOUR_ACCESS_TOKEN');
76
+ console.log(`Total transactions: ${transactions.length}`);
77
+ ```
78
+
79
+ @tab Python
80
+ ```python
81
+ import requests
82
+
83
+ def get_all_transactions(access_token, limit=1000):
84
+ all_transactions = []
85
+ offset = 0
86
+ has_more = True
87
+
88
+ headers = {
89
+ 'Authorization': f'Bearer {access_token}'
90
+ }
91
+
92
+ while has_more:
93
+ url = f'https://api.lunchmoney.dev/v2/transactions?limit={limit}&offset={offset}'
94
+ response = requests.get(url, headers=headers)
95
+ data = response.json()
96
+
97
+ all_transactions.extend(data['transactions'])
98
+ has_more = data['has_more']
99
+ offset += limit
100
+
101
+ print(f"Fetched {len(data['transactions'])} transactions (total: {len(all_transactions)})")
102
+
103
+ return all_transactions
104
+
105
+ # Usage
106
+ transactions = get_all_transactions('YOUR_ACCESS_TOKEN')
107
+ print(f"Total transactions: {len(transactions)}")
108
+ ```
109
+
110
+ @tab cURL
111
+ ```bash
112
+ #!/bin/bash
113
+
114
+ ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
115
+ LIMIT=1000
116
+ OFFSET=0
117
+ HAS_MORE=true
118
+ ALL_TRANSACTIONS="[]"
119
+
120
+ while [ "$HAS_MORE" = "true" ]; do
121
+ echo "Fetching transactions with offset=$OFFSET, limit=$LIMIT..."
122
+
123
+ RESPONSE=$(curl -s -X GET \
124
+ "https://api.lunchmoney.dev/v2/transactions?limit=$LIMIT&offset=$OFFSET" \
125
+ -H "Authorization: Bearer $ACCESS_TOKEN")
126
+
127
+ TRANSACTIONS=$(echo $RESPONSE | jq -r '.transactions')
128
+ HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
129
+
130
+ # Merge transactions (simplified - in practice you'd want proper JSON merging)
131
+ COUNT=$(echo $TRANSACTIONS | jq 'length')
132
+ echo "Fetched $COUNT transactions"
133
+
134
+ OFFSET=$((OFFSET + LIMIT))
135
+ done
136
+
137
+ echo "All transactions fetched"
138
+ ```
139
+ :::
140
+
141
+ ### Pagination with Date Filters
142
+
143
+ Pagination works seamlessly with date range filters:
144
+
145
+ :::tabs
146
+ @tab JavaScript/Node.js
147
+ ```javascript
148
+ async function getTransactionsByDateRange(accessToken, startDate, endDate) {
149
+ const allTransactions = [];
150
+ let offset = 0;
151
+ let hasMore = true;
152
+ const limit = 1000;
153
+
154
+ while (hasMore) {
155
+ const url = new URL('https://api.lunchmoney.dev/v2/transactions');
156
+ url.searchParams.set('start_date', startDate);
157
+ url.searchParams.set('end_date', endDate);
158
+ url.searchParams.set('limit', limit.toString());
159
+ url.searchParams.set('offset', offset.toString());
160
+
161
+ const response = await fetch(url, {
162
+ headers: {
163
+ 'Authorization': `Bearer ${accessToken}`
164
+ }
165
+ });
166
+
167
+ const data = await response.json();
168
+ allTransactions.push(...data.transactions);
169
+
170
+ hasMore = data.has_more;
171
+ offset += limit;
172
+ }
173
+
174
+ return allTransactions;
175
+ }
176
+
177
+ // Usage: Get all transactions for January 2024
178
+ const janTransactions = await getTransactionsByDateRange(
179
+ 'YOUR_ACCESS_TOKEN',
180
+ '2024-01-01',
181
+ '2024-01-31'
182
+ );
183
+ ```
184
+
185
+ @tab Python
186
+ ```python
187
+ import requests
188
+ from urllib.parse import urlencode
189
+
190
+ def get_transactions_by_date_range(access_token, start_date, end_date):
191
+ all_transactions = []
192
+ offset = 0
193
+ has_more = True
194
+ limit = 1000
195
+
196
+ headers = {'Authorization': f'Bearer {access_token}'}
197
+
198
+ while has_more:
199
+ params = {
200
+ 'start_date': start_date,
201
+ 'end_date': end_date,
202
+ 'limit': limit,
203
+ 'offset': offset
204
+ }
205
+
206
+ url = f"https://api.lunchmoney.dev/v2/transactions?{urlencode(params)}"
207
+ response = requests.get(url, headers=headers)
208
+ data = response.json()
209
+
210
+ all_transactions.extend(data['transactions'])
211
+ has_more = data['has_more']
212
+ offset += limit
213
+
214
+ return all_transactions
215
+
216
+ # Usage: Get all transactions for January 2024
217
+ jan_transactions = get_transactions_by_date_range(
218
+ 'YOUR_ACCESS_TOKEN',
219
+ '2024-01-01',
220
+ '2024-01-31'
221
+ )
222
+ ```
223
+
224
+ @tab cURL
225
+ ```bash
226
+ #!/bin/bash
227
+
228
+ ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
229
+ START_DATE="2024-01-01"
230
+ END_DATE="2024-01-31"
231
+ LIMIT=1000
232
+ OFFSET=0
233
+ HAS_MORE=true
234
+
235
+ while [ "$HAS_MORE" = "true" ]; do
236
+ RESPONSE=$(curl -s -X GET \
237
+ "https://api.lunchmoney.dev/v2/transactions?start_date=$START_DATE&end_date=$END_DATE&limit=$LIMIT&offset=$OFFSET" \
238
+ -H "Authorization: Bearer $ACCESS_TOKEN")
239
+
240
+ HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
241
+ COUNT=$(echo $RESPONSE | jq -r '.transactions | length')
242
+ echo "Fetched $COUNT transactions (offset=$OFFSET)"
243
+
244
+ OFFSET=$((OFFSET + LIMIT))
245
+ done
246
+ ```
247
+ :::
248
+
249
+ ### Handling the `has_more` Flag
250
+
251
+ Always check the `has_more` property to determine if you need to fetch more pages:
252
+
253
+ :::tabs
254
+ @tab JavaScript/Node.js
255
+ ```javascript
256
+ async function fetchTransactionsPage(accessToken, limit = 1000, offset = 0) {
257
+ const url = `https://api.lunchmoney.dev/v2/transactions?limit=${limit}&offset=${offset}`;
258
+ const response = await fetch(url, {
259
+ headers: {
260
+ 'Authorization': `Bearer ${accessToken}`
261
+ }
262
+ });
263
+
264
+ const data = await response.json();
265
+
266
+ return {
267
+ transactions: data.transactions,
268
+ hasMore: data.has_more,
269
+ nextOffset: data.has_more ? offset + limit : null
270
+ };
271
+ }
272
+
273
+ // Usage
274
+ const page1 = await fetchTransactionsPage('YOUR_ACCESS_TOKEN', 1000, 0);
275
+ console.log(`Page 1: ${page1.transactions.length} transactions`);
276
+ console.log(`Has more: ${page1.hasMore}`);
277
+
278
+ if (page1.hasMore) {
279
+ const page2 = await fetchTransactionsPage('YOUR_ACCESS_TOKEN', 1000, page1.nextOffset);
280
+ console.log(`Page 2: ${page2.transactions.length} transactions`);
281
+ }
282
+ ```
283
+
284
+ @tab Python
285
+ ```python
286
+ import requests
287
+
288
+ def fetch_transactions_page(access_token, limit=1000, offset=0):
289
+ url = f'https://api.lunchmoney.dev/v2/transactions?limit={limit}&offset={offset}'
290
+ headers = {'Authorization': f'Bearer {access_token}'}
291
+
292
+ response = requests.get(url, headers=headers)
293
+ data = response.json()
294
+
295
+ return {
296
+ 'transactions': data['transactions'],
297
+ 'has_more': data['has_more'],
298
+ 'next_offset': offset + limit if data['has_more'] else None
299
+ }
300
+
301
+ # Usage
302
+ page1 = fetch_transactions_page('YOUR_ACCESS_TOKEN', 1000, 0)
303
+ print(f"Page 1: {len(page1['transactions'])} transactions")
304
+ print(f"Has more: {page1['has_more']}")
305
+
306
+ if page1['has_more']:
307
+ page2 = fetch_transactions_page('YOUR_ACCESS_TOKEN', 1000, page1['next_offset'])
308
+ print(f"Page 2: {len(page2['transactions'])} transactions")
309
+ ```
310
+
311
+ @tab cURL
312
+ ```bash
313
+ ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
314
+ LIMIT=1000
315
+ OFFSET=0
316
+
317
+ RESPONSE=$(curl -s -X GET \
318
+ "https://api.lunchmoney.dev/v2/transactions?limit=$LIMIT&offset=$OFFSET" \
319
+ -H "Authorization: Bearer $ACCESS_TOKEN")
320
+
321
+ HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
322
+ COUNT=$(echo $RESPONSE | jq -r '.transactions | length')
323
+
324
+ echo "Page 1: $COUNT transactions"
325
+ echo "Has more: $HAS_MORE"
326
+
327
+ if [ "$HAS_MORE" = "true" ]; then
328
+ OFFSET=$((OFFSET + LIMIT))
329
+ RESPONSE=$(curl -s -X GET \
330
+ "https://api.lunchmoney.dev/v2/transactions?limit=$LIMIT&offset=$OFFSET" \
331
+ -H "Authorization: Bearer $ACCESS_TOKEN")
332
+ COUNT=$(echo $RESPONSE | jq -r '.transactions | length')
333
+ echo "Page 2: $COUNT transactions"
334
+ fi
335
+ ```
336
+ :::
337
+
338
+ ## Best Practices
339
+
340
+ ### 1. Use Appropriate Limit Values
341
+
342
+ Choose a `limit` value that balances performance and number of requests:
343
+
344
+ - **Small datasets (< 1000 transactions)**: Use the default limit of 1000 to fetch everything in one request
345
+ - **Large datasets**: Consider using smaller limits (e.g., 500) if you need to process transactions incrementally or show progress to users
346
+ - **Maximum efficiency**: Use the maximum limit of 2000 to minimize the number of requests, but be aware of response size and processing time
347
+
348
+ ```javascript
349
+ // Good: Use default limit for most cases
350
+ const response = await fetch('/v2/transactions?limit=1000');
351
+
352
+ // Also good: Use smaller limit for incremental processing
353
+ const response = await fetch('/v2/transactions?limit=500');
354
+ ```
355
+
356
+ ### 2. Always Check `has_more`
357
+
358
+ Don't assume you've received all results. Always check the `has_more` property:
359
+
360
+ ```javascript
361
+ // ❌ Bad: Assumes one page is enough
362
+ const data = await fetch('/v2/transactions').then(r => r.json());
363
+ const transactions = data.transactions; // Might be incomplete!
364
+
365
+ // ✅ Good: Checks for more pages
366
+ let allTransactions = [];
367
+ let offset = 0;
368
+ let hasMore = true;
369
+
370
+ while (hasMore) {
371
+ const data = await fetch(`/v2/transactions?offset=${offset}`).then(r => r.json());
372
+ allTransactions.push(...data.transactions);
373
+ hasMore = data.has_more;
374
+ offset += 1000;
375
+ }
376
+ ```
377
+
378
+ ### 3. Combine with Date Ranges When Possible
379
+
380
+ If you know the date range you need, use `start_date` and `end_date` parameters to reduce the result set size:
381
+
382
+ ```javascript
383
+ // ✅ Good: Uses date range to limit results
384
+ const response = await fetch(
385
+ '/v2/transactions?start_date=2024-01-01&end_date=2024-01-31'
386
+ );
387
+
388
+ // ❌ Less efficient: Fetches all transactions then filters client-side
389
+ const allTransactions = await fetchAllTransactions();
390
+ const janTransactions = allTransactions.filter(t =>
391
+ t.date >= '2024-01-01' && t.date <= '2024-01-31'
392
+ );
393
+ ```
394
+
395
+ ### 4. Handle Edge Cases
396
+
397
+ - **Empty results**: The `transactions` array will be empty, and `has_more` will be `false`
398
+ - **Exact page boundary**: If the number of results is exactly divisible by your limit, `has_more` will be `false` on the last page
399
+ - **Offset beyond results**: If you set an offset larger than the total number of results, you'll get an empty array with `has_more: false`
400
+
401
+ ```javascript
402
+ async function safeFetchTransactions(accessToken, limit = 1000, offset = 0) {
403
+ const response = await fetch(
404
+ `https://api.lunchmoney.dev/v2/transactions?limit=${limit}&offset=${offset}`,
405
+ {
406
+ headers: { 'Authorization': `Bearer ${accessToken}` }
407
+ }
408
+ );
409
+
410
+ if (!response.ok) {
411
+ throw new Error(`API error: ${response.status}`);
412
+ }
413
+
414
+ const data = await response.json();
415
+
416
+ // Handle empty results
417
+ if (data.transactions.length === 0 && !data.has_more) {
418
+ console.log('No transactions found');
419
+ return [];
420
+ }
421
+
422
+ return data;
423
+ }
424
+ ```
425
+
426
+ ### 5. Implement Rate Limit Awareness
427
+
428
+ When paginating through many pages, be mindful of rate limits. Consider adding delays between requests:
429
+
430
+ ```javascript
431
+ async function getAllTransactionsWithRateLimit(accessToken) {
432
+ const allTransactions = [];
433
+ let offset = 0;
434
+ let hasMore = true;
435
+ const limit = 1000;
436
+
437
+ while (hasMore) {
438
+ const data = await fetchTransactionsPage(accessToken, limit, offset);
439
+ allTransactions.push(...data.transactions);
440
+ hasMore = data.has_more;
441
+ offset += limit;
442
+
443
+ // Small delay to avoid hitting rate limits
444
+ if (hasMore) {
445
+ await new Promise(resolve => setTimeout(resolve, 100));
446
+ }
447
+ }
448
+
449
+ return allTransactions;
450
+ }
451
+ ```
452
+
453
+ ## Troubleshooting
454
+
455
+ ### Common Issues
456
+
457
+ **Issue**: Getting duplicate transactions across pages
458
+
459
+ **Solution**: Ensure you're incrementing `offset` by the same value as your `limit`. If you use `limit=1000`, increment `offset` by 1000 for each subsequent page.
460
+
461
+ ```javascript
462
+ // ❌ Wrong: Offset not incremented correctly
463
+ let offset = 0;
464
+ const page1 = await fetch(`/v2/transactions?limit=1000&offset=${offset}`);
465
+ offset += 500; // Wrong! Should be 1000
466
+ const page2 = await fetch(`/v2/transactions?limit=1000&offset=${offset}`);
467
+
468
+ // ✅ Correct: Offset matches limit
469
+ let offset = 0;
470
+ const page1 = await fetch(`/v2/transactions?limit=1000&offset=${offset}`);
471
+ offset += 1000; // Correct!
472
+ const page2 = await fetch(`/v2/transactions?limit=1000&offset=${offset}`);
473
+ ```
474
+
475
+ **Issue**: `has_more` is always `true` in a loop
476
+
477
+ **Solution**: Make sure you're updating both `hasMore` and `offset` in your loop condition:
478
+
479
+ ```javascript
480
+ // ❌ Wrong: Infinite loop
481
+ let offset = 0;
482
+ while (true) { // Never updates hasMore!
483
+ const data = await fetch(`/v2/transactions?offset=${offset}`);
484
+ // Missing: hasMore = data.has_more
485
+ offset += 1000;
486
+ }
487
+
488
+ // ✅ Correct: Proper loop condition
489
+ let offset = 0;
490
+ let hasMore = true;
491
+ while (hasMore) {
492
+ const data = await fetch(`/v2/transactions?offset=${offset}`);
493
+ hasMore = data.has_more; // Update the condition
494
+ offset += 1000;
495
+ }
496
+ ```
497
+
498
+ **Issue**: Getting fewer transactions than expected
499
+
500
+ **Solution**: Remember that filters (date ranges, account IDs, etc.) are applied before pagination. If you're filtering, the total number of transactions will be smaller:
501
+
502
+ ```javascript
503
+ // If you have 5000 total transactions but filter to a date range with only 200 transactions,
504
+ // you'll get 200 transactions total, not 5000
505
+ const data = await fetch('/v2/transactions?start_date=2024-01-01&end_date=2024-01-31');
506
+ // data.transactions.length might be 200, and has_more will be false
507
+ ```
508
+
509
+ ## Need Help?
510
+
511
+ If you're experiencing pagination issues that can't be resolved through the techniques described above:
512
+
513
+ - Review your pagination logic to ensure `offset` is being incremented correctly
514
+ - Verify that you're checking the `has_more` property on each response
515
+ - Consider using date range filters to reduce the result set size
516
+ - [Email our developer advocate](mailto:jp@lunchmoney.app) if you need assistance with pagination implementation
517
+