@lunch-money/developer-docs 2.11.0 → 2.11.1-docs.2
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.
- package/docs/README.md +1 -0
- package/docs/STYLE_GUIDE.md +88 -0
- package/docs/amounts-and-balances.md +5 -5
- package/docs/beta-introduction.md +5 -5
- package/docs/branding-your-app.md +59 -0
- package/docs/currencies.md +8 -9
- package/docs/getting-started.md +14 -14
- package/docs/introduction.md +5 -5
- package/docs/locales.md +124 -0
- package/docs/pagination.md +43 -44
- package/docs/rate-limiting.md +45 -45
- package/docs/using-with-ai.md +2 -2
- package/manifest.json +19 -1
- package/package.json +1 -1
- package/v1/_user.md +2 -2
- package/v2/docs/intro-to-v2.md +12 -12
- package/v2/docs/migration-guide.md +10 -7
- package/v2/docs/version-history.md +8 -0
- package/v2/images/powered-by-lunch-money-badge.png +0 -0
- package/v2/spec/lunch-money-api-v2.yaml +804 -19
package/docs/pagination.md
CHANGED
|
@@ -6,10 +6,10 @@ The Lunch Money API uses offset-based pagination for the `GET /transactions` end
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
> [!NOTE] Pagination
|
|
9
|
+
> [!NOTE] Pagination scope
|
|
10
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
11
|
|
|
12
|
-
## How
|
|
12
|
+
## How pagination works
|
|
13
13
|
|
|
14
14
|
### Parameters
|
|
15
15
|
|
|
@@ -24,13 +24,13 @@ When retrieving transactions using `GET /transactions`, the API supports paginat
|
|
|
24
24
|
- Minimum: `0`
|
|
25
25
|
- Use this to fetch subsequent pages of results
|
|
26
26
|
|
|
27
|
-
### Response
|
|
27
|
+
### Response property
|
|
28
28
|
|
|
29
29
|
- **`has_more`**: A boolean property in the response indicating whether more transactions are available
|
|
30
30
|
- `true`: More transactions match your filter criteria and are available on subsequent pages
|
|
31
31
|
- `false`: All matching transactions have been returned
|
|
32
32
|
|
|
33
|
-
### Basic
|
|
33
|
+
### Basic pagination flow
|
|
34
34
|
|
|
35
35
|
1. Make your first request (optionally with `limit` specified, defaults to 1000)
|
|
36
36
|
2. Check the `has_more` property in the response
|
|
@@ -39,7 +39,7 @@ When retrieving transactions using `GET /transactions`, the API supports paginat
|
|
|
39
39
|
|
|
40
40
|
## Examples
|
|
41
41
|
|
|
42
|
-
### Basic
|
|
42
|
+
### Basic pagination
|
|
43
43
|
|
|
44
44
|
Here's how to fetch transactions page by page:
|
|
45
45
|
|
|
@@ -61,10 +61,10 @@ async function getAllTransactions(accessToken, limit = 1000) {
|
|
|
61
61
|
|
|
62
62
|
const data = await response.json();
|
|
63
63
|
allTransactions.push(...data.transactions);
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
hasMore = data.has_more;
|
|
66
66
|
offset += limit;
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
console.log(`Fetched ${data.transactions.length} transactions (total: ${allTransactions.length})`);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -84,22 +84,22 @@ def get_all_transactions(access_token, limit=1000):
|
|
|
84
84
|
all_transactions = []
|
|
85
85
|
offset = 0
|
|
86
86
|
has_more = True
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
headers = {
|
|
89
89
|
'Authorization': f'Bearer {access_token}'
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
while has_more:
|
|
93
93
|
url = f'https://api.lunchmoney.dev/v2/transactions?limit={limit}&offset={offset}'
|
|
94
94
|
response = requests.get(url, headers=headers)
|
|
95
95
|
data = response.json()
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
all_transactions.extend(data['transactions'])
|
|
98
98
|
has_more = data['has_more']
|
|
99
99
|
offset += limit
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
print(f"Fetched {len(data['transactions'])} transactions (total: {len(all_transactions)})")
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
return all_transactions
|
|
104
104
|
|
|
105
105
|
# Usage
|
|
@@ -119,18 +119,18 @@ ALL_TRANSACTIONS="[]"
|
|
|
119
119
|
|
|
120
120
|
while [ "$HAS_MORE" = "true" ]; do
|
|
121
121
|
echo "Fetching transactions with offset=$OFFSET, limit=$LIMIT..."
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
RESPONSE=$(curl -s -X GET \
|
|
124
124
|
"https://api.lunchmoney.dev/v2/transactions?limit=$LIMIT&offset=$OFFSET" \
|
|
125
125
|
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
TRANSACTIONS=$(echo $RESPONSE | jq -r '.transactions')
|
|
128
128
|
HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
# Merge transactions (simplified - in practice you'd want proper JSON merging)
|
|
131
131
|
COUNT=$(echo $TRANSACTIONS | jq 'length')
|
|
132
132
|
echo "Fetched $COUNT transactions"
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
OFFSET=$((OFFSET + LIMIT))
|
|
135
135
|
done
|
|
136
136
|
|
|
@@ -138,7 +138,7 @@ echo "All transactions fetched"
|
|
|
138
138
|
```
|
|
139
139
|
:::
|
|
140
140
|
|
|
141
|
-
### Pagination with
|
|
141
|
+
### Pagination with date filters
|
|
142
142
|
|
|
143
143
|
Pagination works seamlessly with date range filters:
|
|
144
144
|
|
|
@@ -166,7 +166,7 @@ async function getTransactionsByDateRange(accessToken, startDate, endDate) {
|
|
|
166
166
|
|
|
167
167
|
const data = await response.json();
|
|
168
168
|
allTransactions.push(...data.transactions);
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
hasMore = data.has_more;
|
|
171
171
|
offset += limit;
|
|
172
172
|
}
|
|
@@ -192,9 +192,9 @@ def get_transactions_by_date_range(access_token, start_date, end_date):
|
|
|
192
192
|
offset = 0
|
|
193
193
|
has_more = True
|
|
194
194
|
limit = 1000
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
while has_more:
|
|
199
199
|
params = {
|
|
200
200
|
'start_date': start_date,
|
|
@@ -202,15 +202,15 @@ def get_transactions_by_date_range(access_token, start_date, end_date):
|
|
|
202
202
|
'limit': limit,
|
|
203
203
|
'offset': offset
|
|
204
204
|
}
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
url = f"https://api.lunchmoney.dev/v2/transactions?{urlencode(params)}"
|
|
207
207
|
response = requests.get(url, headers=headers)
|
|
208
208
|
data = response.json()
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
all_transactions.extend(data['transactions'])
|
|
211
211
|
has_more = data['has_more']
|
|
212
212
|
offset += limit
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
return all_transactions
|
|
215
215
|
|
|
216
216
|
# Usage: Get all transactions for January 2024
|
|
@@ -236,17 +236,17 @@ while [ "$HAS_MORE" = "true" ]; do
|
|
|
236
236
|
RESPONSE=$(curl -s -X GET \
|
|
237
237
|
"https://api.lunchmoney.dev/v2/transactions?start_date=$START_DATE&end_date=$END_DATE&limit=$LIMIT&offset=$OFFSET" \
|
|
238
238
|
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
239
|
-
|
|
239
|
+
|
|
240
240
|
HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
|
|
241
241
|
COUNT=$(echo $RESPONSE | jq -r '.transactions | length')
|
|
242
242
|
echo "Fetched $COUNT transactions (offset=$OFFSET)"
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
OFFSET=$((OFFSET + LIMIT))
|
|
245
245
|
done
|
|
246
246
|
```
|
|
247
247
|
:::
|
|
248
248
|
|
|
249
|
-
### Handling the `has_more`
|
|
249
|
+
### Handling the `has_more` flag
|
|
250
250
|
|
|
251
251
|
Always check the `has_more` property to determine if you need to fetch more pages:
|
|
252
252
|
|
|
@@ -262,7 +262,7 @@ async function fetchTransactionsPage(accessToken, limit = 1000, offset = 0) {
|
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
const data = await response.json();
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
return {
|
|
267
267
|
transactions: data.transactions,
|
|
268
268
|
hasMore: data.has_more,
|
|
@@ -288,10 +288,10 @@ import requests
|
|
|
288
288
|
def fetch_transactions_page(access_token, limit=1000, offset=0):
|
|
289
289
|
url = f'https://api.lunchmoney.dev/v2/transactions?limit={limit}&offset={offset}'
|
|
290
290
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
291
|
-
|
|
291
|
+
|
|
292
292
|
response = requests.get(url, headers=headers)
|
|
293
293
|
data = response.json()
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
return {
|
|
296
296
|
'transactions': data['transactions'],
|
|
297
297
|
'has_more': data['has_more'],
|
|
@@ -335,9 +335,9 @@ fi
|
|
|
335
335
|
```
|
|
336
336
|
:::
|
|
337
337
|
|
|
338
|
-
## Best
|
|
338
|
+
## Best practices
|
|
339
339
|
|
|
340
|
-
### 1. Use
|
|
340
|
+
### 1. Use appropriate limit values
|
|
341
341
|
|
|
342
342
|
Choose a `limit` value that balances performance and number of requests:
|
|
343
343
|
|
|
@@ -353,7 +353,7 @@ const response = await fetch('/v2/transactions?limit=1000');
|
|
|
353
353
|
const response = await fetch('/v2/transactions?limit=500');
|
|
354
354
|
```
|
|
355
355
|
|
|
356
|
-
### 2. Always
|
|
356
|
+
### 2. Always check `has_more`
|
|
357
357
|
|
|
358
358
|
Don't assume you've received all results. Always check the `has_more` property:
|
|
359
359
|
|
|
@@ -375,7 +375,7 @@ while (hasMore) {
|
|
|
375
375
|
}
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
-
### 3. Combine with
|
|
378
|
+
### 3. Combine with date ranges when possible
|
|
379
379
|
|
|
380
380
|
If you know the date range you need, use `start_date` and `end_date` parameters to reduce the result set size:
|
|
381
381
|
|
|
@@ -387,12 +387,12 @@ const response = await fetch(
|
|
|
387
387
|
|
|
388
388
|
// ❌ Less efficient: Fetches all transactions then filters client-side
|
|
389
389
|
const allTransactions = await fetchAllTransactions();
|
|
390
|
-
const janTransactions = allTransactions.filter(t =>
|
|
390
|
+
const janTransactions = allTransactions.filter(t =>
|
|
391
391
|
t.date >= '2024-01-01' && t.date <= '2024-01-31'
|
|
392
392
|
);
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
-
### 4. Handle
|
|
395
|
+
### 4. Handle edge cases
|
|
396
396
|
|
|
397
397
|
- **Empty results**: The `transactions` array will be empty, and `has_more` will be `false`
|
|
398
398
|
- **Exact page boundary**: If the number of results is exactly divisible by your limit, `has_more` will be `false` on the last page
|
|
@@ -406,24 +406,24 @@ async function safeFetchTransactions(accessToken, limit = 1000, offset = 0) {
|
|
|
406
406
|
headers: { 'Authorization': `Bearer ${accessToken}` }
|
|
407
407
|
}
|
|
408
408
|
);
|
|
409
|
-
|
|
409
|
+
|
|
410
410
|
if (!response.ok) {
|
|
411
411
|
throw new Error(`API error: ${response.status}`);
|
|
412
412
|
}
|
|
413
|
-
|
|
413
|
+
|
|
414
414
|
const data = await response.json();
|
|
415
|
-
|
|
415
|
+
|
|
416
416
|
// Handle empty results
|
|
417
417
|
if (data.transactions.length === 0 && !data.has_more) {
|
|
418
418
|
console.log('No transactions found');
|
|
419
419
|
return [];
|
|
420
420
|
}
|
|
421
|
-
|
|
421
|
+
|
|
422
422
|
return data;
|
|
423
423
|
}
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
-
### 5. Implement
|
|
426
|
+
### 5. Implement rate limit awareness
|
|
427
427
|
|
|
428
428
|
When paginating through many pages, be mindful of rate limits. Consider adding delays between requests:
|
|
429
429
|
|
|
@@ -439,7 +439,7 @@ async function getAllTransactionsWithRateLimit(accessToken) {
|
|
|
439
439
|
allTransactions.push(...data.transactions);
|
|
440
440
|
hasMore = data.has_more;
|
|
441
441
|
offset += limit;
|
|
442
|
-
|
|
442
|
+
|
|
443
443
|
// Small delay to avoid hitting rate limits
|
|
444
444
|
if (hasMore) {
|
|
445
445
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -452,7 +452,7 @@ async function getAllTransactionsWithRateLimit(accessToken) {
|
|
|
452
452
|
|
|
453
453
|
## Troubleshooting
|
|
454
454
|
|
|
455
|
-
### Common
|
|
455
|
+
### Common issues
|
|
456
456
|
|
|
457
457
|
**Issue**: Getting duplicate transactions across pages
|
|
458
458
|
|
|
@@ -506,7 +506,7 @@ const data = await fetch('/v2/transactions?start_date=2024-01-01&end_date=2024-0
|
|
|
506
506
|
// data.transactions.length might be 200, and has_more will be false
|
|
507
507
|
```
|
|
508
508
|
|
|
509
|
-
## Need
|
|
509
|
+
## Need help?
|
|
510
510
|
|
|
511
511
|
If you're experiencing pagination issues that can't be resolved through the techniques described above:
|
|
512
512
|
|
|
@@ -514,4 +514,3 @@ If you're experiencing pagination issues that can't be resolved through the tech
|
|
|
514
514
|
- Verify that you're checking the `has_more` property on each response
|
|
515
515
|
- Consider using date range filters to reduce the result set size
|
|
516
516
|
- [Email our developer advocate](mailto:jp@lunchmoney.app) if you need assistance with pagination implementation
|
|
517
|
-
|
package/docs/rate-limiting.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Rate
|
|
1
|
+
# Rate limiting
|
|
2
2
|
|
|
3
3
|
The Lunch Money API implements rate limiting to ensure fair usage and prevent abuse of API resources. This guide explains how rate limiting works and how to monitor your rate limit status using response headers.
|
|
4
4
|
|
|
@@ -9,20 +9,20 @@ Rate limiting is applied to all requests to the `/v1/` and `/v2/` endpoints and
|
|
|
9
9
|
|
|
10
10
|
If you exceed this limit, you will receive a `429 Too Many Requests` response.
|
|
11
11
|
|
|
12
|
-
> [!NOTE] Rate
|
|
12
|
+
> [!NOTE] Rate limit scope
|
|
13
13
|
> Rate limiting is applied per IP address. Requests from the same IP address share the same rate limit quotas.
|
|
14
14
|
|
|
15
|
-
## Rate
|
|
15
|
+
## Rate limit response
|
|
16
16
|
|
|
17
17
|
When you exceed a rate limit, the API returns a `429 Too Many Requests` response:
|
|
18
18
|
|
|
19
|
-
### HTTP
|
|
19
|
+
### HTTP status code
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
429 Too Many Requests
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
### Response
|
|
25
|
+
### Response body
|
|
26
26
|
|
|
27
27
|
The response body follows the standard v2 error format:
|
|
28
28
|
|
|
@@ -37,11 +37,11 @@ The response body follows the standard v2 error format:
|
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
### Response
|
|
40
|
+
### Response headers
|
|
41
41
|
|
|
42
42
|
The API includes rate limit information in response headers for all requests (both successful and rate-limited). You can inspect these headers to monitor your current rate limit status and determine when you can make additional requests.
|
|
43
43
|
|
|
44
|
-
#### Standard
|
|
44
|
+
#### Standard headers (RFC Draft 7)
|
|
45
45
|
|
|
46
46
|
The API includes standard rate limit headers in all responses:
|
|
47
47
|
|
|
@@ -56,12 +56,12 @@ RateLimit-Remaining: 3
|
|
|
56
56
|
RateLimit-Reset: 1704067200
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
#### Legacy
|
|
59
|
+
#### Legacy headers
|
|
60
60
|
|
|
61
61
|
For backward compatibility, the API also includes legacy `X-RateLimit-*` headers:
|
|
62
62
|
|
|
63
63
|
- **`X-RateLimit-Limit`**: The maximum number of requests allowed per window
|
|
64
|
-
- **`X-RateLimit-Remaining`**: The number of requests remaining in the current window
|
|
64
|
+
- **`X-RateLimit-Remaining`**: The number of requests remaining in the current window
|
|
65
65
|
- **`X-RateLimit-Reset`**: The time (in seconds since Unix epoch) when the rate limit window resets
|
|
66
66
|
|
|
67
67
|
Example headers:
|
|
@@ -71,7 +71,7 @@ X-RateLimit-Remaining: 3
|
|
|
71
71
|
X-RateLimit-Reset: 1704067200
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
#### Retry-After
|
|
74
|
+
#### `Retry-After` header
|
|
75
75
|
|
|
76
76
|
When you receive a `429 Too Many Requests` response, the API includes a `Retry-After` header indicating how many seconds you should wait before retrying:
|
|
77
77
|
|
|
@@ -81,12 +81,12 @@ Retry-After: 45
|
|
|
81
81
|
|
|
82
82
|
This value represents the time until the rate limit window resets (rounded up to the nearest second).
|
|
83
83
|
|
|
84
|
-
> [!TIP] Using Retry-After
|
|
84
|
+
> [!TIP] Using `Retry-After`
|
|
85
85
|
> Always respect the `Retry-After` header value when implementing retry logic. Waiting for the specified duration ensures you don't waste requests on premature retries.
|
|
86
86
|
|
|
87
|
-
## Monitoring
|
|
87
|
+
## Monitoring rate limits
|
|
88
88
|
|
|
89
|
-
### Reading
|
|
89
|
+
### Reading rate limit headers
|
|
90
90
|
|
|
91
91
|
You can monitor your rate limit status by inspecting the headers in every API response, even successful ones. This allows you to proactively slow down your request rate before hitting the limit.
|
|
92
92
|
|
|
@@ -95,12 +95,12 @@ You can monitor your rate limit status by inspecting the headers in every API re
|
|
|
95
95
|
```javascript
|
|
96
96
|
async function makeRequest(url, options) {
|
|
97
97
|
const response = await fetch(url, options);
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Check rate limit status from headers
|
|
100
100
|
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
|
|
101
101
|
const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '0');
|
|
102
102
|
const resetTime = parseInt(response.headers.get('X-RateLimit-Reset') || '0');
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
if (remaining < 5) {
|
|
105
105
|
console.warn(`Rate limit warning: ${remaining}/${limit} requests remaining`);
|
|
106
106
|
const waitTime = resetTime - Math.floor(Date.now() / 1000);
|
|
@@ -108,13 +108,13 @@ async function makeRequest(url, options) {
|
|
|
108
108
|
console.log(`Rate limit resets in ${waitTime} seconds`);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
if (response.status === 429) {
|
|
113
113
|
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
|
|
114
114
|
console.error(`Rate limited! Retry after ${retryAfter} seconds`);
|
|
115
115
|
throw new Error(`Rate limited: retry after ${retryAfter}s`);
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
return response;
|
|
119
119
|
}
|
|
120
120
|
```
|
|
@@ -126,23 +126,23 @@ import time
|
|
|
126
126
|
|
|
127
127
|
def make_request(url, headers):
|
|
128
128
|
response = requests.get(url, headers=headers)
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
# Check rate limit status
|
|
131
131
|
remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
|
|
132
132
|
limit = int(response.headers.get('X-RateLimit-Limit', 0))
|
|
133
133
|
reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
if remaining < 5:
|
|
136
136
|
print(f"Rate limit warning: {remaining}/{limit} requests remaining")
|
|
137
137
|
wait_time = reset_time - int(time.time())
|
|
138
138
|
if wait_time > 0:
|
|
139
139
|
print(f"Rate limit resets in {wait_time} seconds")
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
if response.status_code == 429:
|
|
142
142
|
retry_after = int(response.headers.get('Retry-After', 60))
|
|
143
143
|
print(f"Rate limited! Retry after {retry_after} seconds")
|
|
144
144
|
raise Exception(f"Rate limited: retry after {retry_after}s")
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
return response
|
|
147
147
|
```
|
|
148
148
|
|
|
@@ -150,7 +150,7 @@ def make_request(url, headers):
|
|
|
150
150
|
```bash
|
|
151
151
|
# Make a request and capture headers
|
|
152
152
|
response=$(curl -s -D /tmp/headers.txt -o /tmp/body.txt -w "%{http_code}" \
|
|
153
|
-
|
|
153
|
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
|
154
154
|
"https://api.lunchmoney.dev/v2/me")
|
|
155
155
|
|
|
156
156
|
# Extract rate limit headers
|
|
@@ -170,7 +170,7 @@ fi
|
|
|
170
170
|
```
|
|
171
171
|
:::
|
|
172
172
|
|
|
173
|
-
### Implementing
|
|
173
|
+
### Implementing exponential backoff
|
|
174
174
|
|
|
175
175
|
When you receive a `429` response, implement exponential backoff with jitter to avoid overwhelming the API:
|
|
176
176
|
|
|
@@ -178,28 +178,28 @@ When you receive a `429` response, implement exponential backoff with jitter to
|
|
|
178
178
|
async function makeRequestWithRetry(url, options, maxRetries = 3) {
|
|
179
179
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
180
180
|
const response = await fetch(url, options);
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
if (response.status !== 429) {
|
|
183
183
|
return response;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
// Get retry-after header or use exponential backoff
|
|
187
187
|
const retryAfter = parseInt(response.headers.get('Retry-After') || '0');
|
|
188
|
-
const waitTime = retryAfter > 0
|
|
188
|
+
const waitTime = retryAfter > 0
|
|
189
189
|
? retryAfter * 1000 // Convert seconds to milliseconds
|
|
190
190
|
: Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000);
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
console.log(`Rate limited. Waiting ${waitTime}ms before retry ${attempt + 1}/${maxRetries}`);
|
|
193
193
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
throw new Error('Max retries exceeded due to rate limiting');
|
|
197
197
|
}
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
## Best
|
|
200
|
+
## Best practices
|
|
201
201
|
|
|
202
|
-
### 1. Monitor
|
|
202
|
+
### 1. Monitor headers proactively
|
|
203
203
|
|
|
204
204
|
Don't wait for a `429` response. Check rate limit headers on every request and adjust your request rate accordingly:
|
|
205
205
|
|
|
@@ -211,7 +211,7 @@ if (remaining < threshold) {
|
|
|
211
211
|
}
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
### 2. Implement
|
|
214
|
+
### 2. Implement request queuing
|
|
215
215
|
|
|
216
216
|
For applications that need to make many requests, implement a queue system that respects rate limits:
|
|
217
217
|
|
|
@@ -222,14 +222,14 @@ class RateLimitedQueue {
|
|
|
222
222
|
this.remaining = 100; // Start with max
|
|
223
223
|
this.resetTime = Date.now() + (15 * 60 * 1000);
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
async enqueue(requestFn) {
|
|
227
227
|
return new Promise((resolve, reject) => {
|
|
228
228
|
this.queue.push({ requestFn, resolve, reject });
|
|
229
229
|
this.processQueue();
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
|
-
|
|
232
|
+
|
|
233
233
|
async processQueue() {
|
|
234
234
|
if (this.queue.length === 0 || this.remaining <= 0) {
|
|
235
235
|
if (this.remaining <= 0 && this.resetTime > Date.now()) {
|
|
@@ -238,15 +238,15 @@ class RateLimitedQueue {
|
|
|
238
238
|
}
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
const { requestFn, resolve, reject } = this.queue.shift();
|
|
243
243
|
try {
|
|
244
244
|
const response = await requestFn();
|
|
245
|
-
|
|
245
|
+
|
|
246
246
|
// Update rate limit status from headers
|
|
247
247
|
this.remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
|
|
248
248
|
this.resetTime = parseInt(response.headers.get('X-RateLimit-Reset') || '0') * 1000;
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
resolve(response);
|
|
251
251
|
this.processQueue(); // Process next item
|
|
252
252
|
} catch (error) {
|
|
@@ -256,7 +256,7 @@ class RateLimitedQueue {
|
|
|
256
256
|
}
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
-
### 3. Cache
|
|
259
|
+
### 3. Cache responses when possible
|
|
260
260
|
|
|
261
261
|
Reduce the number of API calls by caching responses locally:
|
|
262
262
|
|
|
@@ -267,24 +267,24 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
267
267
|
async function getCachedRequest(url, options) {
|
|
268
268
|
const cacheKey = `${url}:${JSON.stringify(options)}`;
|
|
269
269
|
const cached = cache.get(cacheKey);
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
272
272
|
return cached.data;
|
|
273
273
|
}
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
const response = await fetch(url, options);
|
|
276
276
|
const data = await response.json();
|
|
277
|
-
|
|
277
|
+
|
|
278
278
|
cache.set(cacheKey, {
|
|
279
279
|
data,
|
|
280
280
|
timestamp: Date.now()
|
|
281
281
|
});
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
return data;
|
|
284
284
|
}
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
### 4. Batch
|
|
287
|
+
### 4. Batch operations when available
|
|
288
288
|
|
|
289
289
|
Use bulk endpoints when available to reduce the number of requests:
|
|
290
290
|
|
|
@@ -309,7 +309,7 @@ await fetch('/v2/categories', {
|
|
|
309
309
|
```
|
|
310
310
|
:::
|
|
311
311
|
|
|
312
|
-
### 5. Limit
|
|
312
|
+
### 5. Limit crypto refresh polling
|
|
313
313
|
|
|
314
314
|
Crypto refresh requests can be bursty if you poll too often. For `POST /v2/crypto/synced/{id}/refresh`:
|
|
315
315
|
|
|
@@ -319,14 +319,14 @@ Crypto refresh requests can be bursty if you poll too often. For `POST /v2/crypt
|
|
|
319
319
|
|
|
320
320
|
## Troubleshooting
|
|
321
321
|
|
|
322
|
-
### Common
|
|
322
|
+
### Common issues
|
|
323
323
|
|
|
324
324
|
**Issue**: Rate limits resetting unexpectedly
|
|
325
325
|
|
|
326
326
|
**Solution**: Rate limits are tracked per IP address. If you're behind a proxy or load balancer, multiple clients may share the same IP and exhaust the shared quota.
|
|
327
327
|
|
|
328
328
|
|
|
329
|
-
## Need
|
|
329
|
+
## Need help?
|
|
330
330
|
|
|
331
331
|
If you're experiencing rate limiting issues that can't be resolved through the techniques described above:
|
|
332
332
|
|
package/docs/using-with-ai.md
CHANGED
|
@@ -73,7 +73,7 @@ const token = process.env.LUNCH_MONEY_ACCESS_TOKEN;
|
|
|
73
73
|
> [!WARNING]
|
|
74
74
|
> Avoid pasting your access token directly into an AI chat window. Treat it like a password — store it in an environment variable, a `.env` file excluded from source control, or your operating system's secret store.
|
|
75
75
|
|
|
76
|
-
## Best
|
|
76
|
+
## Best practices
|
|
77
77
|
|
|
78
78
|
1. **Start with a test budget** — API changes are permanent. Use a test budget while you're learning so your real data stays safe. See the [Getting Started guide](/getting-started) for instructions.
|
|
79
79
|
|
|
@@ -97,7 +97,7 @@ If you want to experiment, here are a few low-risk starting points:
|
|
|
97
97
|
|
|
98
98
|
Start small, and use a test budget while you're learning.
|
|
99
99
|
|
|
100
|
-
## Need
|
|
100
|
+
## Need help?
|
|
101
101
|
|
|
102
102
|
- Join the <a href="https://lunchmoney.app/discord" target="_blank" rel="noopener noreferrer">Lunch Money Discord</a> and ask in the **#developer-api** channel
|
|
103
103
|
- [Email our developer advocate](mailto:jp@lunchmoney.app)
|
package/manifest.json
CHANGED
|
@@ -41,6 +41,14 @@
|
|
|
41
41
|
"type": "markdown",
|
|
42
42
|
"aliases": ["/v2/currencies"]
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
"path": "/locales",
|
|
46
|
+
"file": "docs/locales.md",
|
|
47
|
+
"title": "Supported Locales",
|
|
48
|
+
"section": "GUIDES",
|
|
49
|
+
"type": "markdown",
|
|
50
|
+
"aliases": ["/v2/locales"]
|
|
51
|
+
},
|
|
44
52
|
{
|
|
45
53
|
"path": "/amounts-and-balances",
|
|
46
54
|
"file": "docs/amounts-and-balances.md",
|
|
@@ -57,6 +65,14 @@
|
|
|
57
65
|
"type": "markdown",
|
|
58
66
|
"aliases": ["/v2/using-with-ai"]
|
|
59
67
|
},
|
|
68
|
+
{
|
|
69
|
+
"path": "/branding-your-app",
|
|
70
|
+
"file": "docs/branding-your-app.md",
|
|
71
|
+
"title": "Branding your App",
|
|
72
|
+
"section": "GUIDES",
|
|
73
|
+
"type": "markdown",
|
|
74
|
+
"aliases": ["/v2/branding-your-app"]
|
|
75
|
+
},
|
|
60
76
|
{
|
|
61
77
|
"path": "/v2/overview",
|
|
62
78
|
"file": "v2/docs/intro-to-v2.md",
|
|
@@ -191,7 +207,9 @@
|
|
|
191
207
|
{ "label": "Pagination", "path": "/pagination" },
|
|
192
208
|
{ "label": "Rate Limiting", "path": "/rate-limits" },
|
|
193
209
|
{ "label": "Supported Currencies", "path": "/currencies" },
|
|
194
|
-
{ "label": "
|
|
210
|
+
{ "label": "Supported Locales", "path": "/locales" },
|
|
211
|
+
{ "label": "Using the API with AI", "path": "/using-with-ai" },
|
|
212
|
+
{ "label": "Branding your App", "path": "/branding-your-app" }
|
|
195
213
|
]},
|
|
196
214
|
{ "section": "REFERENCE", "items": [
|
|
197
215
|
{ "label": "v2 API Overview", "path": "/v2/overview" },
|
package/package.json
CHANGED
package/v1/_user.md
CHANGED
|
@@ -11,8 +11,8 @@ This API (v1) will not progress to a generally available state. For new projects
|
|
|
11
11
|
| user_id | number | false | Unique identifier for user |
|
|
12
12
|
| user_name | string | false | User's name |
|
|
13
13
|
| user_email | string | false | User's email |
|
|
14
|
-
| account_id | number | false | Unique identifier for the associated
|
|
15
|
-
| budget_name | string | false | Name of the associated
|
|
14
|
+
| account_id | number | false | Unique identifier for the associated budget account |
|
|
15
|
+
| budget_name | string | false | Name of the associated budget account |
|
|
16
16
|
| primary_currency|string | false | Primary currency from user's settings |
|
|
17
17
|
| api_key_label | string | true | User-defined label of the developer API key used. Returns null if nothing has been set. |
|
|
18
18
|
|