@lunch-money/developer-docs 2.11.1-preview.8 → 2.11.2-preview.1

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.
@@ -1,4 +1,4 @@
1
- # Rate Limiting
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 Limit Scope
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 Limit Response
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 Status Code
19
+ ### HTTP status code
20
20
 
21
21
  ```
22
22
  429 Too Many Requests
23
23
  ```
24
24
 
25
- ### Response Body
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 Headers
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 Headers (RFC Draft 7)
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 Headers
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 Header
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 Rate Limits
87
+ ## Monitoring rate limits
88
88
 
89
- ### Reading Rate Limit Headers
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
- -H "Authorization: Bearer YOUR_TOKEN" \
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 Exponential Backoff
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 Practices
200
+ ## Best practices
201
201
 
202
- ### 1. Monitor Headers Proactively
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 Request Queuing
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 Responses When Possible
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 Operations When Available
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 Crypto Refresh Polling
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 Issues
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 Help?
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
 
@@ -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 Practices
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 Help?
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
@@ -73,6 +73,94 @@
73
73
  "type": "markdown",
74
74
  "aliases": ["/v2/branding-your-app"]
75
75
  },
76
+ {
77
+ "path": "/oauth",
78
+ "file": "docs/oauth/index.md",
79
+ "title": "OAuth Overview",
80
+ "section": "OAUTH",
81
+ "type": "markdown",
82
+ "aliases": ["/v2/oauth", "/v2/oauth/"]
83
+ },
84
+ {
85
+ "path": "/oauth/concepts",
86
+ "file": "docs/oauth/concepts.md",
87
+ "title": "OAuth Concepts",
88
+ "section": "OAUTH",
89
+ "type": "markdown",
90
+ "aliases": ["/v2/oauth/concepts"]
91
+ },
92
+ {
93
+ "path": "/oauth/register-client",
94
+ "file": "docs/oauth/register-client.md",
95
+ "title": "Register an OAuth Client",
96
+ "section": "OAUTH",
97
+ "type": "markdown",
98
+ "aliases": ["/v2/oauth/register-client"]
99
+ },
100
+ {
101
+ "path": "/oauth/authorization-code",
102
+ "file": "docs/oauth/authorization-code.md",
103
+ "title": "Implement Authorization",
104
+ "section": "OAUTH",
105
+ "type": "markdown",
106
+ "aliases": ["/v2/oauth/authorization-code"]
107
+ },
108
+ {
109
+ "path": "/oauth/development",
110
+ "file": "docs/oauth/development.md",
111
+ "title": "Develop and Test",
112
+ "section": "OAUTH",
113
+ "type": "markdown",
114
+ "aliases": ["/v2/oauth/development"]
115
+ },
116
+ {
117
+ "path": "/oauth/tokens",
118
+ "file": "docs/oauth/tokens.md",
119
+ "title": "Token Lifecycle",
120
+ "section": "OAUTH",
121
+ "type": "markdown",
122
+ "aliases": ["/v2/oauth/tokens"]
123
+ },
124
+ {
125
+ "path": "/oauth/security",
126
+ "file": "docs/oauth/security.md",
127
+ "title": "OAuth Security",
128
+ "section": "OAUTH",
129
+ "type": "markdown",
130
+ "aliases": ["/v2/oauth/security"]
131
+ },
132
+ {
133
+ "path": "/oauth/troubleshooting",
134
+ "file": "docs/oauth/troubleshooting.md",
135
+ "title": "Troubleshoot OAuth",
136
+ "section": "OAUTH",
137
+ "type": "markdown",
138
+ "aliases": ["/v2/oauth/troubleshooting"]
139
+ },
140
+ {
141
+ "path": "/oauth/review-and-approval",
142
+ "file": "docs/oauth/review-and-approval.md",
143
+ "title": "OAuth Client Review and Approval",
144
+ "section": "OAUTH",
145
+ "type": "markdown",
146
+ "aliases": ["/v2/oauth/review-and-approval"]
147
+ },
148
+ {
149
+ "path": "/oauth/native-apps",
150
+ "file": "docs/oauth/native-apps.md",
151
+ "title": "Native Apps",
152
+ "section": "OAUTH",
153
+ "type": "markdown",
154
+ "aliases": ["/v2/oauth/native-apps"]
155
+ },
156
+ {
157
+ "path": "/oauth/scopes",
158
+ "file": "docs/oauth/scopes.md",
159
+ "title": "OAuth Scopes",
160
+ "section": "OAUTH",
161
+ "type": "markdown",
162
+ "aliases": ["/v2/oauth/scopes"]
163
+ },
76
164
  {
77
165
  "path": "/v2/overview",
78
166
  "file": "v2/docs/intro-to-v2.md",
@@ -211,6 +299,19 @@
211
299
  { "label": "Using the API with AI", "path": "/using-with-ai" },
212
300
  { "label": "Branding your App", "path": "/branding-your-app" }
213
301
  ]},
302
+ { "section": "OAUTH", "collapsed": true, "items": [
303
+ { "label": "OAuth Overview", "path": "/oauth" },
304
+ { "label": "Concepts", "path": "/oauth/concepts" },
305
+ { "label": "Register a Client", "path": "/oauth/register-client" },
306
+ { "label": "Implement Authorization", "path": "/oauth/authorization-code" },
307
+ { "label": "Develop and Test", "path": "/oauth/development" },
308
+ { "label": "Token Lifecycle", "path": "/oauth/tokens" },
309
+ { "label": "Security", "path": "/oauth/security" },
310
+ { "label": "Troubleshooting", "path": "/oauth/troubleshooting" },
311
+ { "label": "Review and Approval", "path": "/oauth/review-and-approval" },
312
+ { "label": "Native Apps", "path": "/oauth/native-apps" },
313
+ { "label": "Scope Catalog", "path": "/oauth/scopes" }
314
+ ]},
214
315
  { "section": "REFERENCE", "items": [
215
316
  { "label": "v2 API Overview", "path": "/v2/overview" },
216
317
  { "label": "v2 API Reference", "path": "/v2/docs", "external": true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunch-money/developer-docs",
3
- "version": "2.11.1-preview.8",
3
+ "version": "2.11.2-preview.1",
4
4
  "description": "Developer documentation content for Lunch Money APIs",
5
5
  "exports": {
6
6
  ".": "./package.json",
@@ -18,8 +18,8 @@
18
18
  "access": "public"
19
19
  },
20
20
  "scripts": {
21
- "validate:manifest": "node scripts/validate-manifest.js",
22
- "prepack": "node scripts/validate-manifest.js && node scripts/prepack.js",
21
+ "validate:manifest": "node ../../scripts/generate-oauth-scope-catalog.js --check && node scripts/validate-manifest.js",
22
+ "prepack": "node ../../scripts/generate-oauth-scope-catalog.js --check && node scripts/validate-manifest.js && node scripts/prepack.js",
23
23
  "postpack": "node scripts/postpack.js"
24
24
  },
25
25
  "keywords": [
@@ -1,4 +1,4 @@
1
- # v2 API Overview
1
+ # v2 API overview
2
2
 
3
3
  Welcome to the Lunch Money v2 API. This page covers the core concepts you need to work with the API effectively — authentication, response shapes, naming conventions, error handling, and more. If you're migrating from v1, see the [Migration Guide](./migration-guide.md).
4
4
 
@@ -10,7 +10,7 @@ All API requests require a Bearer token in the `Authorization` header:
10
10
  Authorization: Bearer YOUR_ACCESS_TOKEN
11
11
  ```
12
12
 
13
- Get your access token from the <a href="https://my.lunchmoney.app/developers" target="_blank" rel="noopener noreferrer">Lunch Money developers page</a>. See the [Getting Started guide](/v2/getting-started) for a step-by-step walkthrough including how to create a test budget.
13
+ Use a personal access token for API exploration or work with your own budgeting account. The [Getting started guide](/getting-started) walks through creating one on the <a href="https://my.lunchmoney.app/developers" target="_blank" rel="noopener noreferrer">Lunch Money Developers page</a> and making your first request. Applications built for other Lunch Money users should use an [OAuth access token issued after the user authorizes the application](/oauth).
14
14
 
15
15
  ## Base URL
16
16
 
@@ -18,7 +18,7 @@ Get your access token from the <a href="https://my.lunchmoney.app/developers" ta
18
18
  https://api.lunchmoney.dev/v2
19
19
  ```
20
20
 
21
- ## Response Shapes
21
+ ## Response shapes
22
22
 
23
23
  The v2 API uses consistent HTTP status codes and response bodies across all endpoints.
24
24
 
@@ -39,7 +39,7 @@ All errors return an appropriate 4XX status code — the v2 API never returns an
39
39
  | `401` | Unauthorized — missing or invalid Bearer token |
40
40
  | `404` | Not Found — the requested object does not exist |
41
41
  | `422` | Unprocessable Content — request is valid but cannot be fulfilled (e.g., deleting a category with dependents) |
42
- | `429` | Too Many Requests — rate limit exceeded; response includes a `Retry-After` header. See the [Rate Limiting guide](/v2/rate-limits). |
42
+ | `429` | Too Many Requests — rate limit exceeded; response includes a `Retry-After` header. See the [Rate limiting guide](/rate-limits). |
43
43
 
44
44
  All error responses share a consistent body format:
45
45
 
@@ -52,7 +52,7 @@ All error responses share a consistent body format:
52
52
  }
53
53
  ```
54
54
 
55
- > [!NOTE] Atomic Operations
55
+ > [!NOTE] Atomic operations
56
56
  > When a request fails, no part of it is applied to your data. For example, if one transaction in a bulk insert has a validation error, none of the transactions in that request are inserted. Fix the errors and retry — you don't need to worry about partial duplicates.
57
57
  >
58
58
  > If a request has multiple validation errors, the response will attempt to list as many as possible, but it is not guaranteed to catch all errors in a single response.
@@ -61,7 +61,7 @@ All error responses share a consistent body format:
61
61
 
62
62
  The v2 API validates all requests strictly. A `400` is returned if the request includes unexpected parameters, missing required fields, fields with invalid values, or unexpected properties in the request body. This makes it easier to catch mistakes during development.
63
63
 
64
- ## Naming Conventions
64
+ ## Naming conventions
65
65
 
66
66
  Object properties follow consistent rules across the entire API:
67
67
 
@@ -72,11 +72,11 @@ Object properties follow consistent rules across the entire API:
72
72
  - Child objects of the same type are always in a `children` property
73
73
  - All `id` fields are `integer` type (not `number`)
74
74
 
75
- ## Amounts, Balances & Sign Convention
75
+ ## Amounts, balances & sign convention
76
76
 
77
- The v2 API uses a consistent set of properties — `amount`, `balance`, `currency`, and `to_base` — across all objects that deal with money. See the [Amounts & Balances guide](/v2/amounts-and-balances) for a full explanation of how these properties work, including the sign convention and multi-currency support.
77
+ The v2 API uses a consistent set of properties — `amount`, `balance`, `currency`, and `to_base` — across all objects that deal with money. See the [Amounts & balances guide](/amounts-and-balances) for a full explanation of how these properties work, including the sign convention and multi-currency support.
78
78
 
79
- ## Hydrated vs. Non-Hydrated Responses
79
+ ## Hydrated vs. non-hydrated responses
80
80
 
81
81
  In v1, some endpoints returned "hydrated" objects — related object details were embedded directly in the response alongside their IDs. For example, a transaction included `category_name`, `asset_name`, and an array of full tag objects.
82
82
 
@@ -93,17 +93,17 @@ In v2, responses are **non-hydrated**: related objects are returned as IDs only.
93
93
 
94
94
  This improves response times and keeps the API consistent. For apps that need related object details frequently, maintaining a local cache of categories, accounts, and tags is the recommended pattern.
95
95
 
96
- ## Updating Objects (PUT)
96
+ ## Updating objects (PUT)
97
97
 
98
98
  Most objects in the v2 API are updatable via `PUT /endpoint/{id}`. The rules are consistent:
99
99
 
100
100
  - Request bodies may contain any mix of **user-definable** properties (will be updated) and **system-defined** properties like `id` or `created_at` (accepted but ignored).
101
101
  - Any unexpected property that is neither user-definable nor system-defined will fail validation and return `400`.
102
102
 
103
- > [!WARNING] Overwrite Behavior
103
+ > [!WARNING] Overwrite behavior
104
104
  > Complex properties — objects and arrays — are **replaced entirely** by whatever is in the request body. To add a tag to a transaction without removing existing tags, first fetch the transaction, add the new tag ID to the existing `tag_ids` array, then PUT the full updated array.
105
105
 
106
- > [!TIP] Recommended Pattern
106
+ > [!TIP] Recommended pattern
107
107
  > Because system-defined properties are tolerated in PUT requests, you can safely GET an object, modify the properties you want to change, and PUT the whole thing back without stripping system fields first.
108
108
 
109
109
  ## Versioning
@@ -99,7 +99,7 @@ One change worth noting: all `id` fields are now explicitly `integer` type (not
99
99
 
100
100
  #### New Properties
101
101
 
102
- - `debits_as_negative`: A user preference shown in the Lunch Money app. This no longer changes how transaction amounts are represented in the v2 API, but apps may use this setting to display amounts and balances in the way the user prefers. See the [Amounts & Balances guide](/v2/amounts-and-balances) for details.
102
+ - `debits_as_negative`: A user preference shown in the Lunch Money app. This no longer changes how transaction amounts are represented in the v2 API, but apps may use this setting to display amounts and balances in the way the user prefers. See the [Amounts & balances guide](/amounts-and-balances) for details.
103
103
 
104
104
  <a href="./changelog-visual#user-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 differences</a>
105
105
 
@@ -386,7 +386,7 @@ GET /v2/transactions
386
386
  > [!WARNING] Behavior Change
387
387
  > In the v1 API the optional `debit_as_negative` query parameter could change how transaction amounts were represented. In the v2 API, transaction amount signs are fixed: positive values are debits and negative values are credits. Existing applications that assume positive values are credits need to be updated.
388
388
 
389
- See the [Amounts & Balances guide](/v2/amounts-and-balances) for the full sign convention and how `to_base` works for multi-currency accounts.
389
+ See the [Amounts & balances guide](/amounts-and-balances) for the full sign convention and how `to_base` works for multi-currency accounts.
390
390
 
391
391
 
392
392
  > [!TIP] Migration Tip
@@ -5,11 +5,15 @@ The Lunch Money API spec uses a modified version of SEMVER for its versioning me
5
5
  - The minor version represents the number of main endpoints the current version of the spec supports. For example, a version of the API that supports the /me, /categories, and /transactions endpoints would have a minor version of 3.
6
6
  - The revision number represents the number of updates since the last endpoint was added. For example, each time changes are made to one of the existing three APIs as described above, the revision number will be bumped.
7
7
 
8
+ ## v2.11.2 - TBD
9
+ - Add OAuth 2.0 bearer-token authentication to the v2 API, allowing applications to access Lunch Money on behalf of users who authorize them
10
+ - Enforce required OAuth scopes for every v2 operation and publish the scope and scope-group catalog mapped to V2 `operationId` values
11
+ - Document the OAuth 2.0 authorization-code flow used to obtain access tokens
12
+
8
13
  ## v2.11.1 - TBD
9
14
  - Add `GET /me/account/settings` and `PUT /me/account/settings` for account-level settings
10
15
  - Add `GET /me/user/settings` and `PUT /me/user/settings` for user-level display and formatting preferences
11
16
  - Add `GET /me/user/account/settings` and `PUT /me/user/account/settings` for settings specific to a user and budgeting account
12
- from `GET /me/account/settings` and `PUT /me/account/settings` to `GET /me/user/account/settings` and `PUT /me/user/account/settings`
13
17
  - Add `include_pending_in_totals` to `GET /me/account/settings` and `PUT /me/account/settings`
14
18
  - Add `default_manual_account_id` to `GET /me/user/account/settings` and `PUT /me/user/account/settings`
15
19
  - Document `GET /budgets/settings` response schema publicly; change `budget_period_quantity` to integer
@@ -0,0 +1,32 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1120" height="780" viewBox="0 0 1120 780" role="img" aria-labelledby="title desc">
2
+ <title id="title">Lunch Money OAuth authorization-code flow</title>
3
+ <desc id="desc">A sequence diagram with three actors: your application, the Lunch Money user, and Lunch Money. The application redirects the user's browser to Lunch Money. When necessary, Lunch Money presents its sign-in screen and the user signs in directly with Lunch Money. Lunch Money then presents the budgeting-account selector and requested permissions, the user selects an account and approves access, Lunch Money returns an authorization code, and the application exchanges it for tokens and calls the API.</desc>
4
+ <defs>
5
+ <marker id="app-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#b45309"/></marker>
6
+ <marker id="user-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#9a6700"/></marker>
7
+ <marker id="lm-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#0f766e"/></marker>
8
+ </defs>
9
+ <style>
10
+ .actor{stroke-width:2}.app{fill:#ffedd5;stroke:#b45309}.user{fill:#fef3c7;stroke:#9a6700}.lm{fill:#ccfbf1;stroke:#0f766e}
11
+ .actor-label{font:700 19px system-ui,sans-serif;fill:#172b2a}.actor-detail{font:14px system-ui,sans-serif;fill:#334e4b}
12
+ .lifeline{stroke:#94a3b8;stroke-width:2;stroke-dasharray:7 7}.flow{stroke-width:2.5;fill:none}.app-flow{stroke:#b45309;marker-end:url(#app-arrow)}.user-flow{stroke:#9a6700;marker-end:url(#user-arrow)}.lm-flow{stroke:#0f766e;marker-end:url(#lm-arrow)}
13
+ .step-bg{fill:#fff;stroke:#cbd5e1;stroke-width:1}.step{font:600 14px system-ui,sans-serif;fill:#172b2a}
14
+ </style>
15
+ <rect class="actor app" x="40" y="25" width="260" height="72" rx="12"/>
16
+ <text class="actor-label" x="170" y="56" text-anchor="middle">Your application</text><text class="actor-detail" x="170" y="79" text-anchor="middle">browser route and server</text>
17
+ <rect class="actor user" x="430" y="25" width="260" height="72" rx="12"/>
18
+ <text class="actor-label" x="560" y="56" text-anchor="middle">Lunch Money user</text><text class="actor-detail" x="560" y="79" text-anchor="middle">browser, sign-in, and approval</text>
19
+ <rect class="actor lm" x="820" y="25" width="260" height="72" rx="12"/>
20
+ <text class="actor-label" x="950" y="56" text-anchor="middle">Lunch Money</text><text class="actor-detail" x="950" y="79" text-anchor="middle">authorization server and API</text>
21
+ <path class="lifeline" d="M170 105 V750"/><path class="lifeline" d="M560 105 V750"/><path class="lifeline" d="M950 105 V750"/>
22
+ <path class="flow app-flow" d="M170 140 H550"/><rect class="step-bg" x="278" y="118" width="180" height="28" rx="6"/><text class="step" x="368" y="137" text-anchor="middle">1. Redirect the user</text>
23
+ <path class="flow user-flow" d="M560 200 H940"/><rect class="step-bg" x="652" y="178" width="196" height="28" rx="6"/><text class="step" x="750" y="197" text-anchor="middle">2. Request authorization</text>
24
+ <path class="flow lm-flow" d="M950 260 H570"/><rect class="step-bg" x="640" y="238" width="240" height="28" rx="6"/><text class="step" x="760" y="257" text-anchor="middle">3. Show sign-in, when needed</text>
25
+ <path class="flow user-flow" d="M560 320 H940"/><rect class="step-bg" x="605" y="298" width="290" height="28" rx="6"/><text class="step" x="750" y="317" text-anchor="middle">4. Sign in directly with Lunch Money</text>
26
+ <path class="flow lm-flow" d="M950 380 H570"/><rect class="step-bg" x="590" y="358" width="340" height="28" rx="6"/><text class="step" x="760" y="377" text-anchor="middle">5. Present account and permission choices</text>
27
+ <path class="flow user-flow" d="M560 440 H940"/><rect class="step-bg" x="585" y="418" width="330" height="28" rx="6"/><text class="step" x="750" y="437" text-anchor="middle">6. Select budgeting account and approve</text>
28
+ <path class="flow lm-flow" d="M950 500 H180"/><rect class="step-bg" x="385" y="478" width="360" height="28" rx="6"/><text class="step" x="565" y="497" text-anchor="middle">7. Redirect to callback with authorization code</text>
29
+ <path class="flow app-flow" d="M170 580 H940"/><rect class="step-bg" x="335" y="558" width="460" height="28" rx="6"/><text class="step" x="565" y="577" text-anchor="middle">8. Exchange code with PKCE (+ client secret for confidential client)</text>
30
+ <path class="flow lm-flow" d="M950 640 H180"/><rect class="step-bg" x="385" y="618" width="360" height="28" rx="6"/><text class="step" x="565" y="637" text-anchor="middle">9. Return access token + optional refresh token</text>
31
+ <path class="flow app-flow" d="M170 710 H940"/><rect class="step-bg" x="466" y="688" width="198" height="28" rx="6"/><text class="step" x="565" y="707" text-anchor="middle">10. Call the V2 API</text>
32
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="990" height="300" viewBox="0 0 990 300" role="img" aria-labelledby="title desc">
2
+ <title id="title">OAuth token lifecycle and recovery</title>
3
+ <desc id="desc">Authorization yields a short-lived access token and, when offline access applies, a finite refresh token. Refresh replaces tokens. Expiration, revocation, or a terminal refresh failure leads to fresh authorization.</desc>
4
+ <defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#0f766e"/></marker></defs>
5
+ <style>.box{fill:#f0f9f8;stroke:#2a6b64;stroke-width:2}.warn{fill:#fff4dc;stroke:#9a6a16;stroke-width:2}.label{font:600 17px system-ui,sans-serif;fill:#173f3b}.small{font:14px system-ui,sans-serif;fill:#294744}.line{stroke:#0f766e;stroke-width:2.5;fill:none;marker-end:url(#arrow)}.annotation-bg{fill:#fff;stroke:#cbd5e1;stroke-width:1}.annotation{font:600 14px system-ui,sans-serif;fill:#172b2a}</style>
6
+ <rect class="box" x="35" y="65" width="180" height="70" rx="12"/><text class="label" x="125" y="94" text-anchor="middle">Authorization</text><text class="small" x="125" y="117" text-anchor="middle">user present</text>
7
+ <rect class="box" x="300" y="35" width="200" height="70" rx="12"/><text class="label" x="400" y="64" text-anchor="middle">Access token</text><text class="small" x="400" y="87" text-anchor="middle">short-lived</text>
8
+ <rect class="box" x="300" y="165" width="200" height="70" rx="12"/><text class="label" x="400" y="194" text-anchor="middle">Refresh token</text><text class="small" x="400" y="217" text-anchor="middle">finite; offline_access</text>
9
+ <rect class="box" x="590" y="100" width="170" height="70" rx="12"/><text class="label" x="675" y="129" text-anchor="middle">Refresh</text><text class="small" x="675" y="152" text-anchor="middle">store replacements</text>
10
+ <rect class="warn" x="790" y="100" width="140" height="70" rx="12"/><text class="label" x="860" y="129" text-anchor="middle">Terminal failure</text><text class="small" x="860" y="152" text-anchor="middle">reauthorize</text>
11
+ <path class="line" d="M215 88 L290 72"/><path class="line" d="M215 112 L290 185"/><path class="line" d="M500 200 L580 150"/><path class="line" d="M760 135 H780"/><path class="line" d="M675 95 C675 25 500 15 470 35"/>
12
+ <rect class="annotation-bg" x="530" y="10" width="170" height="28" rx="6"/><text class="annotation" x="615" y="29" text-anchor="middle">replacement token set</text>
13
+ <rect class="annotation-bg" x="740" y="198" width="240" height="36" rx="6"/><text class="annotation" x="860" y="221" text-anchor="middle">expired, revoked, or rejected</text>
14
+ </svg>