@lunch-money/developer-docs 2.11.2-preview.2 → 2.11.2-preview.4

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.
@@ -3,7 +3,7 @@
3
3
  Building a tool, plugin, or integration on top of Lunch Money? Use the official **Powered by Lunch Money** badge to credit the connection clearly — without implying that your app *is* Lunch Money.
4
4
 
5
5
  <a href="https://lunchmoney.app/assets/images/media-kit/powered-by-lunch-money-badge.png" download="powered-by-lunch-money-badge.png" target="_blank" rel="noopener noreferrer" title="Download Powered by Lunch Money badge">
6
- <img src="/v2/images/powered-by-lunch-money-badge.png" alt="Powered by Lunch Money badge" />
6
+ <img src="/v2/images/powered-by-lunch-money-badge.png" alt="Powered by Lunch Money badge" width="320" />
7
7
  </a>
8
8
 
9
9
  > [!TIP]
@@ -1,8 +1,8 @@
1
1
  # Implement the authorization-code flow
2
2
 
3
- After you [register an OAuth client](/oauth/register-client), your application uses its settings—including its client ID, client secret, and registered redirect URI—to implement the authorization flow. As shown in the [OAuth overview](/oauth), your application sends the user to Lunch Money, Lunch Money returns an authorization code to your callback, and your application exchanges that code for tokens.
3
+ After you [register an OAuth client](/oauth/register-client), your application uses its settings—including its client ID, client secret, if configured, and a registered redirect URI—to implement the authorization flow. As shown in the [OAuth overview](/oauth), your application sends the user to Lunch Money, Lunch Money returns an authorization code to your callback, and your application exchanges that code for tokens. Step 4 explains how to identify the Lunch Money user and budgeting account associated with the resulting connection.
4
4
 
5
- This guide walks through that process for a **Confidential web client**. Your server handles the callback and token exchange and keeps the client secret and refresh tokens secure. If you are building a standalone mobile or desktop application, read the [native application guidance](/oauth/native-apps) instead.
5
+ This guide walks through that process for a **Confidential web client**. Your server handles the callback and token exchange and keeps the client secret and refresh tokens secure. If you are building a standalone mobile or desktop application, read the [native application guidance](/oauth/native-apps) instead. Expo developers can also follow the [native Expo sample](/oauth/sample-applications#native-expo-sample).
6
6
 
7
7
  ## Choose an OAuth library
8
8
 
@@ -15,7 +15,7 @@ OAuth libraries handle much of the protocol work for you, including generating P
15
15
 
16
16
  For Node.js and TypeScript, [`openid-client`](https://github.com/panva/openid-client) is one maintained option. Despite its name, using it does not mean that Lunch Money supports OpenID Connect identity scopes, ID tokens, or UserInfo.
17
17
 
18
- The [confidential Node.js sample application](/oauth/sample-applications#understand-and-apply-the-teaching-code) shows this library in a complete Lunch Money flow. Begin with its [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/README.md), then follow the listed reading order from protocol types and discovery through authorization, callback validation, credential storage, API access, optional refresh, and revocation.
18
+ The [confidential Node.js sample application](/oauth/sample-applications#understand-the-confidential-teaching-code) shows this library in a complete Lunch Money flow. Begin with its [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/README.md), then follow the listed reading order from protocol types and discovery through authorization, callback validation, credential storage, API access, optional refresh, and revocation.
19
19
 
20
20
  ## Configure Lunch Money with discovery
21
21
 
@@ -42,6 +42,8 @@ On your server, generate:
42
42
 
43
43
  Redirect the browser to the discovered authorization endpoint with `response_type=code`, your `client_id`, exact `redirect_uri`, `state`, `code_challenge`, and `code_challenge_method=S256`.
44
44
 
45
+ Lunch Money handles sign-in, budgeting-account selection, and consent. A user may have multiple budgeting accounts, but this authorization is for the one they select. Keep the authorization attempt bound to the application user who started it.
46
+
45
47
  > [!NOTE] Scopes come from the client registration
46
48
  > You do not need to send `scope`. If your OAuth library includes it, Lunch Money ignores the requested value and uses the client's complete registered scope set. An authorization request cannot narrow or expand that set; changing scopes requires a replacement client.
47
49
 
@@ -75,8 +77,16 @@ Authorization: Bearer YOUR_ACCESS_TOKEN
75
77
 
76
78
  The operation must be allowed by the token's scope and by the user's access to the selected budgeting account. See the [scope catalog](/oauth/scopes).
77
79
 
80
+ `GET /v2/me` requires `me:read`. Its `id` identifies the Lunch Money user, while `account_id` and `budget_name` identify the selected budgeting account. Associate these details with the application user's connection and display the connected budget. When reconnecting, use the new response to establish the account identity rather than assuming the user selected the same budget. See [users, budgeting accounts, and access tokens](/oauth/concepts#users-budgeting-accounts-and-access-tokens).
81
+
82
+ Store the credential set under the authenticated application user's ID and the returned `account_id`. Treat that pair as the owner of the connection: update the same record when the user reauthorizes that budgeting account, and create or select a separate record when they authorize another account. Persist the access token, refresh token when present, scope, expiration metadata, and account identity together. Do not use `budget_name` as the key or try to derive the account identity by parsing the opaque access token.
83
+
84
+ Retain the returned Lunch Money user `id` with the connection as well. Your application establishes ownership through its own authenticated session. When authorization starts, store the authenticated application user's internal ID in the server-side authorization attempt associated with `state`. After validating the callback and calling `GET /v2/me`, store the credentials with that application-user ID and the returned Lunch Money `id`/`account_id` pair.
85
+
86
+ For later requests, look up the selected connection using both its connection ID and the authenticated application user's internal ID. If no matching record exists, reject the request before loading or decrypting credentials. When the same application user reauthorizes a budgeting account, replace credentials only in the record matching that application user and Lunch Money `id`/`account_id` pair. Values supplied by the browser, including a connection ID or `account_id`, select a candidate record but do not establish ownership. See [Associate credentials with your application user](/oauth/security#associate-credentials-with-your-application-user) for the storage and ownership rules.
87
+
78
88
  ## 5. Continue or recover
79
89
 
80
- Use the response's `expires_in` value to schedule renewal slightly before expiry. Read [token lifecycle and recovery](/oauth/tokens) before implementing refresh, logout, or revocation.
90
+ Use the response's `expires_in` value to track the expected lifetime of the access token. You can refresh slightly before expiry as a proactive optimization, or wait for an API request to return `401` and refresh then. In either case, coordinate refreshes for each connection, retry the failed request at most once after a successful refresh, and reauthorize after a terminal refresh failure. Read [token lifecycle and recovery](/oauth/tokens) before implementing refresh, logout, or revocation.
81
91
 
82
92
  Next: [Develop and test your OAuth application](/oauth/development).
@@ -10,6 +10,42 @@ These terms describe who participates in Lunch Money OAuth and what each credent
10
10
  - **Authorization server:** the Lunch Money-owned service that presents the Lunch Money sign-in and consent screens, records the user's decision, and issues authorization codes and tokens. It also handles token requests, including exchanging authorization codes and refresh tokens for new tokens. The user signs in directly with Lunch Money; the third-party application never receives or sees their Lunch Money login credentials.
11
11
  - **Lunch Money API service:** the Lunch Money-owned V2 API, which receives the application's API requests, accepts access tokens, and checks that each request is permitted by the client's scopes.
12
12
 
13
+ ## Users, budgeting accounts, and access tokens
14
+
15
+ A **Lunch Money user** is the person who signs in to Lunch Money. One user can have access to multiple **budgeting accounts**, such as a personal budget, a household budget, and a test budget. Each budgeting account contains its own financial data. Here, “budgeting account” means that collection of data, not an individual bank or manually managed account within it.
16
+
17
+ During OAuth authorization, the user selects one budgeting account and approves your client's permissions. The resulting **OAuth access token is specific to that Lunch Money user and budgeting account pair**, for your client. It does not grant access to every budgeting account the user can access.
18
+
19
+ For example, Alex can access both “Household” and “API testing.” If Alex selects “API testing” during authorization, your application's access token operates on that budgeting account. It cannot use the same token to read or update “Household.” Scopes determine which operations the application may perform within the selected account; they do not add access to other budgeting accounts.
20
+
21
+ When applications store OAuth credentials, they should associate the authorization with the application user who started the flow and keep track of which Lunch Money user and budgeting account that connection represents.
22
+
23
+ ### Identify the authorized user and account
24
+
25
+ If your client has the `me:read` scope, call `GET /v2/me` with the access token. The response identifies both sides of the connection:
26
+
27
+ | Field | Meaning |
28
+ | --- | --- |
29
+ | `id` | The Lunch Money user's ID |
30
+ | `account_id` | The selected budgeting account's ID |
31
+ | `budget_name` | The selected budgeting account's display name |
32
+
33
+ Use the IDs to associate API data with the correct user and budgeting account, and show `budget_name` so the application user can see which budget they are using. Do not treat `id` alone as the budgeting-account identity or use `budget_name` as a unique identifier. Access tokens are opaque credentials; obtain this information from the API response rather than parsing the token.
34
+
35
+ ### Select a different budgeting account
36
+
37
+ Applications should provide a UI that allows users to switch between budgeting accounts they have already authorized through that application, or to authorize another budgeting account.
38
+
39
+ To connect to a new, previously unauthorized, budgeting account, send the user through authorization again and have them select it. Changing a locally stored account ID does not change which account an existing token can access. Refreshing a token renews access for its existing authorization; it does not select another budgeting account.
40
+
41
+ Authorizing a second budgeting account creates or reuses that budget's connection and does not revoke the first one. Update each connection's credentials and account identity together, and keep cached data from one account separate from data loaded for another.
42
+
43
+ ### Reauthorize an existing budgeting account
44
+
45
+ If the user selects a budgeting account that is already connected to your application, treat the authorization as a reconnection of that existing connection. Match it by the stable `account_id`, keep that connection's identity, and atomically replace its stored access-token and refresh-token set with the credentials from the successful exchange. Reauthorizing the same budgeting account reuses that account's grant family; it does not create a second connection or affect the user's other budgeting accounts.
46
+
47
+ When the user chooses to switch between existing authorized budgets, select the saved connection for that `account_id` and use its stored access token to make API calls. The application UI should show which budgeting account is currently active.
48
+
13
49
  ## Client types and credentials
14
50
 
15
51
  When a developer registers a client, they must choose a client type based on whether the application code can keep a client secret secure. This choice cannot be changed after the client is registered. The developer selects either **Confidential web client** or **Native or public client**.
@@ -34,12 +34,14 @@ The user's browser—not Lunch Money's server—connects to the loopback listene
34
34
 
35
35
  ## Repeat or reset authorization
36
36
 
37
- During development, the client owner can run authorization again whenever they want to repeat the sign-in, budgeting-account selection, consent, and callback flow. They do not need to revoke the current token first; a new successful authorization replaces their previous active authorization for that client.
37
+ During development, the client owner can run authorization again whenever they want to repeat the sign-in, budgeting-account selection, consent, and callback flow. They do not need to revoke the current token first. Selecting the same budgeting account reuses that account's grant; selecting a different account creates or reuses a separate connection and leaves the first connection available.
38
38
 
39
39
  To test how the application responds after access is revoked, revoke the current token and make another API request with it. Lunch Money should reject the request, and the application should discard its saved credentials and begin authorization again. The [token revocation guide](/oauth/tokens#revoke-access) includes copyable `curl` requests for development testing.
40
40
 
41
41
  ## Test the complete client
42
42
 
43
+ If the client owner has multiple budgeting accounts, test authorization with a separate test account first, then repeat the flow and select a different account. For a client registered with `me:read`, confirm that `GET /v2/me` returns the same user `id` and the newly selected `account_id`. Store the credentials under the authenticated application user and that account ID, show the selected budget, and do not display cached data from another connection. Switch back to the first saved connection without authorizing again, then verify that revoking or invalidating one connection does not affect the other.
44
+
43
45
  1. Complete authorization. For each attempt, your application should create a random `state` value, save it with the application user's session, and verify that Lunch Money returns the same value to the callback. Rejecting a missing, changed, or reused value prevents a callback started elsewhere from being attached to the wrong session.
44
46
  2. Keep the PKCE verifier with that same authorization attempt and use it when exchanging the returned code. Confirm that an exchange with a missing or changed verifier fails; this prevents someone who intercepts the code from using it.
45
47
  3. Read the token response's `scope` field and confirm that it contains the complete scope set registered for the client. Do not inspect or parse the access token itself.
@@ -50,7 +52,7 @@ To test how the application responds after access is revoked, revoke the current
50
52
 
51
53
  First make sure your application can recover without a refresh token. Record when the token response arrives and use `expires_in` to determine when the access token is expected to expire. After it expires, make a harmless API request and confirm that Lunch Money returns an authentication failure identifying the token as invalid.
52
54
 
53
- Your application should discard the unusable token and start authorization again. Confirm that the same application user can complete the flow, select a budgeting account, and continue using the application with the replacement token. This recovery path is required when a client does not have `offline_access`, and it remains the fallback when a refresh token expires, is revoked, or otherwise stops working.
55
+ Your application should discard the unusable token for that connection and start authorization again. Confirm that the same application user can complete the flow, select the intended budgeting account, and continue using that connection with the replacement token. This recovery path is required when a client does not have `offline_access`, and it remains the fallback when a refresh token expires, is revoked, or otherwise stops working. Keep other connections usable while this one recovers.
54
56
 
55
57
  After reauthorization works, clients registered with `offline_access` can add and test refresh-token handling. Verify replacement-token storage, concurrent-refresh protection, revocation, and terminal refresh recovery before relying on unattended operation. See [token lifecycle and recovery](/oauth/tokens).
56
58
 
@@ -21,6 +21,8 @@ Personal access tokens remain useful for quick work on your own account. OAuth i
21
21
 
22
22
  An OAuth client is an application's registration with Lunch Money. When a developer registers the client, they select the scopes—the specific permissions—the application needs. Later, when a Lunch Money user authorizes the application, Lunch Money shows them that complete permission set before they approve access. Choose only the permissions the application's features require. A focused permission request is easier for users to understand and trust, and reduces the risk of the application reading or changing data it does not need.
23
23
 
24
+ One Lunch Money user can have multiple budgeting accounts and multiple connections to the same OAuth client. Each OAuth access token represents that user and the single budgeting account they selected during authorization. It does not provide access to all of their budgets. Read [users, budgeting accounts, and access tokens](/oauth/concepts#users-budgeting-accounts-and-access-tokens) for an example and how to identify the connected account.
25
+
24
26
  ## Before you start
25
27
 
26
28
  You need an active Lunch Money account to [register and manage an OAuth client](/oauth/applications) for your application. During development, only the client owner can authorize the application. Approval is required before other Lunch Money users can authorize it.
@@ -21,6 +21,18 @@ Native clients may use an HTTP loopback redirect with `localhost`, `127.0.0.1`,
21
21
 
22
22
  Store tokens in Keychain on Apple platforms. On Android, use current Keystore-backed storage guidance from your maintained library; do not start new work with deprecated `EncryptedSharedPreferences` APIs. Coordinate refresh-token use, persist replacements, and fall back to interactive authorization after terminal failure.
23
23
 
24
- Platform code should delegate protocol validation to the maintained library while your application supplies safe configuration, lifecycle storage, API calls, revocation, and user-facing recovery. Full Lunch Money Swift and Kotlin sample apps are not part of this documentation phase; the upstream AppAuth projects provide maintained platform examples.
24
+ ### Keep application users' connections separate
25
+
26
+ A native app can support multiple application users on the same device. If your app has its own sign-in system, keep each user's saved Lunch Money connections in a separate storage namespace. Bind the authorization attempt to the application user who started it, and retain the Lunch Money `id` and `account_id` returned by `GET /v2/me` with each credential set. Only load a connection after verifying that it belongs to the currently authenticated application user. Secure device storage does not perform that application-level ownership check for you.
27
+
28
+ On sign-out or user switching, stop work using the previous user's credentials and clear in-memory tokens, the active connection selection, and cached financial data. Remove saved credentials if your app does not retain connections across sign-ins; otherwise, make them available only after their owner signs in again. Do not attach an OAuth callback or refresh result from the previous session to the newly signed-in user.
29
+
30
+ If your app has no separate user accounts, the device's OS account and app storage protections provide the local access boundary. Anyone allowed to use the unlocked app may be able to use its saved connections; separate budgeting-account records alone do not authenticate different people.
31
+
32
+ Sending tokens to an application backend introduces server-side credential storage. If your architecture does that, apply the [credential ownership rules](/oauth/security#associate-credentials-with-your-application-user) on the backend as well as protecting credentials on the device.
33
+
34
+ Platform code should delegate protocol validation to the maintained library while your application supplies safe configuration, lifecycle storage, API calls, revocation, and user-facing recovery.
35
+
36
+ Lunch Money does not currently provide complete Swift or Kotlin samples; the upstream AppAuth projects provide maintained platform examples. Expo developers can use the [native Expo sample](/oauth/sample-applications#native-expo-sample) for a complete Lunch Money walkthrough and reference implementation.
25
37
 
26
38
  Next: [Review OAuth security guidance](/oauth/security).
@@ -11,7 +11,7 @@ Before you add OAuth to your application code, register an OAuth client with Lun
11
11
 
12
12
  List the Lunch Money features your application will provide, then use the [scope catalog](/oauth/scopes) to choose the smallest complete set of permissions those features require. Select `offline_access` only if the application needs to continue making API requests after the initial access token expires and while the user is away.
13
13
 
14
- For the confidential Node.js sample, always select `me:read`. Add `offline_access` only if you want to exercise its optional refresh branch; the rest of the walkthrough does not require it.
14
+ If your application may support more than one budgeting account for the same application user, always include `me:read`. After authorization, `GET /v2/me` lets your application record the Lunch Money user's `id` and the selected budgeting account's `account_id`, then store each connection under the correct application user and account pair. The same scope is also required when your application only needs to identify the single budgeting account it has connected.
15
15
 
16
16
  Scopes cannot be changed after registration. While you are developing your application, creating a replacement client is inexpensive, so let the scope set evolve as you learn what the application needs. Aim to have the application's core functionality—and its required permissions—settled before you submit the client for review.
17
17
 
@@ -1,13 +1,17 @@
1
1
  # OAuth sample applications
2
2
 
3
- Use a sample application to see Lunch Money OAuth working end to end before you adapt the flow to your own architecture. Samples are separated by OAuth client type and platform because confidential and native clients have different credential boundaries.
3
+ Use a sample application to see Lunch Money OAuth working end to end before you adapt the flow to your own architecture. Choose the confidential sample when a trusted server will hold credentials, or the native sample when an installed iOS or Android application will hold them. The two architectures have different security boundaries and are documented separately.
4
4
 
5
- > [!TIP] Open-source sample
6
- > [View the confidential Node.js sample on GitHub](https://github.com/lunch-money/lm-oauth-confidential-node-example) or begin with its [README](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/README.md).
5
+ > [!TIP] Open-source samples
6
+ > - [Confidential Node.js and TypeScript sample](https://github.com/lunch-money/lm-oauth-confidential-node-example) and its [README](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/README.md)
7
+ > - [Native Expo sample for iOS and Android](https://github.com/lunch-money/lm-oauth-native-expo-example) and its [README](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/README.md)
7
8
 
8
9
  ## Confidential Node.js and TypeScript sample
9
10
 
10
- `lm-oauth-confidential-node-example` is a teaching sample for a **confidential, server-side web client**. It uses Node.js, TypeScript, and the maintained [`openid-client`](https://github.com/panva/openid-client) library to demonstrate:
11
+ > [!NOTE] This walkthrough is for confidential applications
12
+ > The walkthrough and teaching-code guide below apply to the confidential Node.js sample, where a trusted server holds the client secret and credentials. For an installed iOS or Android application that holds its own credentials without a client secret, skip to the [Native Expo sample](#native-expo-sample).
13
+
14
+ [`lm-oauth-confidential-node-example`](https://github.com/lunch-money/lm-oauth-confidential-node-example) is a teaching sample for a **confidential, server-side web client**. It uses Node.js, TypeScript, and the maintained [`openid-client`](https://github.com/panva/openid-client) library to demonstrate:
11
15
 
12
16
  - configuring a registered Lunch Money OAuth client and discovering authorization-server metadata;
13
17
  - starting authorization with single-use `state` and S256 Proof Key for Code Exchange (PKCE);
@@ -21,7 +25,7 @@ Use a sample application to see Lunch Money OAuth working end to end before you
21
25
  > The sample uses a fixed demonstration user and in-memory stores. It does not provide real application authentication, durable multi-user isolation, production credential storage, deployment configuration, or operational controls. Review its [security guidance](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/SECURITY.md) and [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/PRODUCTION_CHECKLIST.md) before adapting any part of it.
22
26
 
23
27
  <!-- BEGIN GENERATED OAUTH SAMPLE WALKTHROUGH -->
24
- <!-- Source: lunch-money/lm-oauth-confidential-node-example@fbf03343847032ef256994758adf397218d63f74:docs/WALKTHROUGH.md -->
28
+ <!-- Source: lunch-money/lm-oauth-confidential-node-example@c8e4d8338d430a3247a48d6e017aaf68d84bd832:docs/WALKTHROUGH.md -->
25
29
  ## Try OAuth with your Lunch Money account
26
30
 
27
31
  Use this walkthrough to authorize a real Lunch Money OAuth client, call the Lunch Money API, optionally refresh its credentials, revoke access, and repeat the flow. The sample is a confidential, server-side web client built with Node.js and TypeScript.
@@ -93,10 +97,10 @@ In the same terminal, set the client ID and secret from the Developer Portal:
93
97
  export OAUTH_CLIENT_ID='YOUR_CLIENT_ID'
94
98
  export OAUTH_CLIENT_SECRET='YOUR_CLIENT_SECRET'
95
99
  export OAUTH_REDIRECT_URI='http://localhost:4002/oauth/callback'
96
- export LUNCH_MONEY_API_BASE_URL='https://api-alpha.lunchmoney.dev/'
100
+ export LUNCH_MONEY_API_BASE_URL='{{LUNCH_MONEY_API_BASE_URL}}'
97
101
  ```
98
102
 
99
- The sample uses `LUNCH_MONEY_API_BASE_URL` for OAuth discovery, token operations, and Lunch Money API requests. A real application should also use [OAuth discovery](https://lunchmoney.dev/oauth/authorization-code#configure-lunch-money-with-discovery) instead of hard-coding authorization, token, and revocation endpoint URLs. `PORT` is optional and defaults to `4002`.
103
+ The Developer Portal replaces `{{LUNCH_MONEY_API_BASE_URL}}` with the API base URL for the environment where you registered the client. If you are reading this walkthrough in the sample repository, use the API base URL provided with your Lunch Money OAuth access. The sample uses that one value for OAuth discovery, token operations, and Lunch Money API requests. A real application should also use [OAuth discovery](https://lunchmoney.dev/oauth/authorization-code#configure-lunch-money-with-discovery) instead of hard-coding authorization, token, and revocation endpoint URLs. `PORT` is optional and defaults to `4002`.
100
104
 
101
105
  `SESSION_SECRET` is also optional for this local sample. If it is absent, the process creates a new random cookie-signing secret when it starts. Because all browser sessions, authorization attempts, and credentials are held in memory and cleared on restart, the generated secret can be cleared at the same time. A production application must instead provide a strong `SESSION_SECRET`, keep it stable across restarts, and not rotate it for each OAuth authorization.
102
106
 
@@ -175,10 +179,10 @@ When access is still active, revoke it first through **Revoke and verify** or Lu
175
179
 
176
180
  After the flow succeeds, connect each action you performed to the code that implemented it, then decide how those responsibilities fit into your own application.
177
181
 
178
- Before adapting the code, read the repository's [OAuth code guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/fbf03343847032ef256994758adf397218d63f74/src/oauth/README.md), [security model](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/fbf03343847032ef256994758adf397218d63f74/SECURITY.md), [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/fbf03343847032ef256994758adf397218d63f74/PRODUCTION_CHECKLIST.md), and [troubleshooting guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/fbf03343847032ef256994758adf397218d63f74/TROUBLESHOOTING.md).
182
+ Before adapting the code, read the repository's [OAuth code guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/c8e4d8338d430a3247a48d6e017aaf68d84bd832/src/oauth/README.md), [security model](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/c8e4d8338d430a3247a48d6e017aaf68d84bd832/SECURITY.md), [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/c8e4d8338d430a3247a48d6e017aaf68d84bd832/PRODUCTION_CHECKLIST.md), and [troubleshooting guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/c8e4d8338d430a3247a48d6e017aaf68d84bd832/TROUBLESHOOTING.md).
179
183
  <!-- END GENERATED OAUTH SAMPLE WALKTHROUGH -->
180
184
 
181
- ## Understand and apply the teaching code
185
+ ## Understand the confidential teaching code
182
186
 
183
187
  The open-source sample you just ran is designed to make the Lunch Money integration code easy to find, understand, and adapt. The modules under `src/oauth` show the OAuth responsibilities that a confidential server-side application needs: discovery, authorization attempts, callback validation, credential ownership and storage, API access, optional refresh, revocation, and safe error handling.
184
188
 
@@ -192,8 +196,28 @@ Start with the [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-confi
192
196
 
193
197
  The OAuth teaching code is isolated under `src/oauth`. Hono, sessions, cookies, pages, and other runnable presentation code live under `src/scaffolding`, so you can replace the framework without obscuring the protocol flow. The sample's [architecture guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/ARCHITECTURE.md) explains this boundary, and its [OAuth tests](https://github.com/lunch-money/lm-oauth-confidential-node-example/tree/main/tests/oauth) mirror the recommended reading order, including focused [refresh tests](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/tests/oauth/refresh.test.ts). If your result differs, compare the stage that failed with the sample's [troubleshooting guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/TROUBLESHOOTING.md) and the public [OAuth troubleshooting guide](/oauth/troubleshooting).
194
198
 
195
- ## Native applications
199
+ ## Native Expo sample
200
+
201
+ Choose [`lm-oauth-native-expo-example`](https://github.com/lunch-money/lm-oauth-native-expo-example) for an iOS or Android application installed
202
+ on the user's device when there is no trusted backend that can hold a client
203
+ secret. It uses Expo, the platform authentication browser, authorization code
204
+ with S256 PKCE, and platform secure storage as a native/public client. Its
205
+ optional `offline_access` path demonstrates rotated refresh credentials and
206
+ terminal reauthorization without introducing a backend or client secret.
207
+
208
+ Do not choose it merely because your interface is responsive. Choose it because
209
+ the installed application performs OAuth and owns its credential. If a trusted
210
+ server performs the code exchange and stores credentials, use the confidential
211
+ Node.js sample above instead.
212
+
213
+ The native repository's [walkthrough](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/docs/WALKTHROUGH.md) is the canonical hands-on guide;
214
+ complete the registration, development-build, authorization, `/v2/me`,
215
+ optional refresh, revocation, and local-reset walkthrough there.
216
+
217
+ ### Follow the native flow through the source
218
+
219
+ Start with the native sample's [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/src/oauth/README.md). It provides the maintained reading order from public configuration and Expo's system-browser adapter through callback validation, secure credential storage, optional refresh, revocation, and the workflow connected to the sample screen.
196
220
 
197
- A separate native/public-client sample is planned but is not yet available. Native mobile and desktop applications use PKCE **without a client secret** and store tokens in platform secure storage. Follow the [native application guidance](/oauth/native-apps); do not adapt the confidential sample by placing its client secret or server-held credentials in installed application code.
221
+ The OAuth teaching code is isolated under `src/oauth`; React Native UI and platform storage adapters live under `src/scaffolding`. Use the sample's [OAuth tests](https://github.com/lunch-money/lm-oauth-native-expo-example/tree/main/tests) to follow the expected success and failure behavior, then review its [security guidance](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/SECURITY.md) and [production checklist](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/PRODUCTION_CHECKLIST.md) before adapting it.
198
222
 
199
223
  Next: [Develop and test your OAuth application](/oauth/development) and [operate it securely](/oauth/security).
@@ -7,7 +7,7 @@ Choose the smallest complete set of permissions your application needs. This pag
7
7
 
8
8
  ## Scope groups
9
9
 
10
- Groups are selection shortcuts, not extra permissions. A group expands to the exact scopes listed below.
10
+ Groups are selection shortcuts, not extra permissions. Every predefined group includes `me:read` so an application can identify the authorized Lunch Money user and budgeting account. A group expands to the exact scopes listed below.
11
11
 
12
12
  ### Read only
13
13
 
@@ -19,25 +19,25 @@ View all supported Lunch Money data without changing it
19
19
 
20
20
  Import, export, organize, and manage transactions and their attachments
21
21
 
22
- [`transactions:read`](#scope-transactions-read), [`transactions:create`](#scope-transactions-create), [`transactions:update`](#scope-transactions-update), [`transactions:delete`](#scope-transactions-delete), [`transaction_attachments:read`](#scope-transaction-attachments-read), [`transaction_attachments:create`](#scope-transaction-attachments-create), [`transaction_attachments:delete`](#scope-transaction-attachments-delete), [`categories:read`](#scope-categories-read), [`tags:read`](#scope-tags-read), [`manual_accounts:read`](#scope-manual-accounts-read), [`plaid_accounts:read`](#scope-plaid-accounts-read), [`recurring_items:read`](#scope-recurring-items-read)
22
+ [`me:read`](#scope-me-read), [`transactions:read`](#scope-transactions-read), [`transactions:create`](#scope-transactions-create), [`transactions:update`](#scope-transactions-update), [`transactions:delete`](#scope-transactions-delete), [`transaction_attachments:read`](#scope-transaction-attachments-read), [`transaction_attachments:create`](#scope-transaction-attachments-create), [`transaction_attachments:delete`](#scope-transaction-attachments-delete), [`categories:read`](#scope-categories-read), [`tags:read`](#scope-tags-read), [`manual_accounts:read`](#scope-manual-accounts-read), [`plaid_accounts:read`](#scope-plaid-accounts-read), [`recurring_items:read`](#scope-recurring-items-read)
23
23
 
24
24
  ### Budgeting
25
25
 
26
26
  Analyze and manage budgets, categories, and tags
27
27
 
28
- [`summary:read`](#scope-summary-read), [`budgets:read`](#scope-budgets-read), [`budgets:update`](#scope-budgets-update), [`budgets:delete`](#scope-budgets-delete), [`categories:read`](#scope-categories-read), [`categories:create`](#scope-categories-create), [`categories:update`](#scope-categories-update), [`categories:delete`](#scope-categories-delete), [`tags:read`](#scope-tags-read), [`tags:create`](#scope-tags-create), [`tags:update`](#scope-tags-update), [`tags:delete`](#scope-tags-delete), [`recurring_items:read`](#scope-recurring-items-read)
28
+ [`me:read`](#scope-me-read), [`summary:read`](#scope-summary-read), [`budgets:read`](#scope-budgets-read), [`budgets:update`](#scope-budgets-update), [`budgets:delete`](#scope-budgets-delete), [`categories:read`](#scope-categories-read), [`categories:create`](#scope-categories-create), [`categories:update`](#scope-categories-update), [`categories:delete`](#scope-categories-delete), [`tags:read`](#scope-tags-read), [`tags:create`](#scope-tags-create), [`tags:update`](#scope-tags-update), [`tags:delete`](#scope-tags-delete), [`recurring_items:read`](#scope-recurring-items-read)
29
29
 
30
30
  ### Accounts
31
31
 
32
32
  Manage accounts, balances, and balance history
33
33
 
34
- [`manual_accounts:read`](#scope-manual-accounts-read), [`manual_accounts:create`](#scope-manual-accounts-create), [`manual_accounts:update`](#scope-manual-accounts-update), [`manual_accounts:delete`](#scope-manual-accounts-delete), [`plaid_accounts:read`](#scope-plaid-accounts-read), [`plaid_accounts:update`](#scope-plaid-accounts-update), [`crypto_manual:read`](#scope-crypto-manual-read), [`crypto_manual:create`](#scope-crypto-manual-create), [`crypto_manual:update`](#scope-crypto-manual-update), [`crypto_manual:delete`](#scope-crypto-manual-delete), [`crypto_synced:read`](#scope-crypto-synced-read), [`crypto_synced:update`](#scope-crypto-synced-update), [`balance_history:read`](#scope-balance-history-read), [`balance_history:update`](#scope-balance-history-update), [`balance_history:delete`](#scope-balance-history-delete)
34
+ [`me:read`](#scope-me-read), [`manual_accounts:read`](#scope-manual-accounts-read), [`manual_accounts:create`](#scope-manual-accounts-create), [`manual_accounts:update`](#scope-manual-accounts-update), [`manual_accounts:delete`](#scope-manual-accounts-delete), [`plaid_accounts:read`](#scope-plaid-accounts-read), [`plaid_accounts:update`](#scope-plaid-accounts-update), [`crypto_manual:read`](#scope-crypto-manual-read), [`crypto_manual:create`](#scope-crypto-manual-create), [`crypto_manual:update`](#scope-crypto-manual-update), [`crypto_manual:delete`](#scope-crypto-manual-delete), [`crypto_synced:read`](#scope-crypto-synced-read), [`crypto_synced:update`](#scope-crypto-synced-update), [`balance_history:read`](#scope-balance-history-read), [`balance_history:update`](#scope-balance-history-update), [`balance_history:delete`](#scope-balance-history-delete)
35
35
 
36
36
  ### Organize
37
37
 
38
38
  Manage categories and tags used to organize transactions
39
39
 
40
- [`categories:read`](#scope-categories-read), [`categories:create`](#scope-categories-create), [`categories:update`](#scope-categories-update), [`categories:delete`](#scope-categories-delete), [`tags:read`](#scope-tags-read), [`tags:create`](#scope-tags-create), [`tags:update`](#scope-tags-update), [`tags:delete`](#scope-tags-delete), [`recurring_items:read`](#scope-recurring-items-read)
40
+ [`me:read`](#scope-me-read), [`categories:read`](#scope-categories-read), [`categories:create`](#scope-categories-create), [`categories:update`](#scope-categories-update), [`categories:delete`](#scope-categories-delete), [`tags:read`](#scope-tags-read), [`tags:create`](#scope-tags-create), [`tags:update`](#scope-tags-update), [`tags:delete`](#scope-tags-delete), [`recurring_items:read`](#scope-recurring-items-read)
41
41
 
42
42
  ### Full access
43
43
 
@@ -26,11 +26,25 @@ If your browser code uses access tokens, keep them available for no longer than
26
26
 
27
27
  A confidential application must authenticate its own user before starting Lunch Money OAuth. Bind each authorization attempt to that authenticated user's server-side session, then recover the owner from the consumed server-side attempt after the callback is verified. Do not let the callback choose who owns the resulting credentials.
28
28
 
29
- Store the verified credential set under a stable internal application-user ID. If one application user may connect more than one Lunch Money context, also assign each connection its own stable internal connection ID. These identifiers belong to your application; do not use an email address, callback parameter, form field, or any other browser-supplied value as the credential owner.
29
+ Store each budgeting-account connection under a stable internal application-user ID. Retain the Lunch Money user `id` and budgeting-account `account_id` returned by `GET /v2/me` alongside that connection's credential set. The application-user ID identifies who may use the saved connection; the Lunch Money IDs identify whose authorization and which budgeting account the tokens represent. Do not use an email address, callback parameter, form field, or other browser-supplied value to establish the credential owner.
30
30
 
31
- Enforce the same ownership boundary every time your backend reads credentials, replaces them after authorization or refresh, revokes them, or deletes them. Durable credentials must be encrypted at rest, and backend access should be limited to the services and operators that need it. Never return refresh tokens or client secrets to browser JavaScript.
31
+ ```text
32
+ Authenticated application user
33
+ -> owned connection (Lunch Money user id + account_id)
34
+ -> encrypted credential set
35
+ ```
32
36
 
33
- The [confidential Node.js sample](/oauth/sample-applications#understand-and-apply-the-teaching-code) makes this boundary explicit in its [`CredentialStore`](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/tokens.ts) abstraction. Its [security guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/SECURITY.md) explains the threat model, and its [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/PRODUCTION_CHECKLIST.md) lists the storage, tenant-isolation, lifecycle, and operational work the teaching sample deliberately omits. Those boundaries matter, but their implementation and database schema are specific to your application.
37
+ When the user returns to your application, authenticate them through your application's own sign-in system before listing or using their saved connections. Knowing a Lunch Money `id`, `account_id`, or internal connection ID does not prove ownership. Limit connection lookups to the authenticated application user's records, and reject requests for a connection they do not own before loading its credentials.
38
+
39
+ For example, if Alex and Sam both use your application, Sam must not be able to use Alex's saved credentials by submitting Alex's connection ID. This remains true even if they both have access to the same Lunch Money budgeting account. Each application user must authorize their own connection. If Alex has authorized two budgeting accounts, both connections belong to Alex's application user and may be listed and selected within Alex's authenticated session. Each connection still uses the credential set for its own Lunch Money `id`/`account_id` pair.
40
+
41
+ Enforce this ownership check for every API request, refresh, reauthorization, revocation, and deletion involving a saved connection. Background jobs must carry the connection's stored owner and identity so they use the same checks without an interactive session. Updating credentials after reauthorization must remain within the original application user's connections and preserve the Lunch Money `id`/`account_id` identity. A different identity must not silently overwrite an existing connection.
42
+
43
+ Durable credentials must be encrypted at rest, and backend access should be limited to the services and operators that need it. Encryption protects stored secrets; your application must still enforce ownership when it reads or uses them. Never return refresh tokens or client secrets to browser JavaScript. List connections using non-secret metadata such as `budget_name`, and keep their credentials in the protected store.
44
+
45
+ Disconnecting one budgeting account should revoke continuing access for that connection and remove its locally stored credentials. Keep the user's other connections intact. On application sign-out or user switching, clear session-specific connection selections and cached data so the next user cannot use the previous user's connection. Signing out of your application and disconnecting Lunch Money are distinct actions; retained connections must remain accessible only through their owner's authenticated session or authorized background jobs.
46
+
47
+ The [confidential Node.js sample](/oauth/sample-applications#understand-the-confidential-teaching-code) makes this boundary explicit in its [`CredentialStore`](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/tokens.ts) abstraction. Its [security guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/SECURITY.md) explains the threat model, and its [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/PRODUCTION_CHECKLIST.md) lists the storage, tenant-isolation, lifecycle, and operational work the teaching sample deliberately omits. Those boundaries matter, but their implementation and database schema are specific to your application.
34
48
 
35
49
  ## Secure authorization and callbacks
36
50
 
@@ -47,7 +61,7 @@ Choose only the resource scopes needed for expected features. Add `offline_acces
47
61
 
48
62
  ## Operate credentials safely
49
63
 
50
- Rotate client secrets on a schedule appropriate for your application's risk and immediately when one may have been exposed. Use overlapping active secrets during a controlled rotation: create the replacement, deploy and verify it, and then revoke the old secret. Coordinate refreshes so two workers do not replay the same rotating token, and atomically persist every complete replacement credential set. Production storage needs database transactions, distributed per-connection locking, recovery when persistence fails, multi-instance coordination, and tenant isolation. Make logout or disconnect invalidate continuing refresh access as well as clear local state.
64
+ Rotate client secrets on a schedule appropriate for your application's risk and immediately when one may have been exposed. Use overlapping active secrets during a controlled rotation: create the replacement, deploy and verify it, and then revoke the old secret. Coordinate refreshes so two workers do not replay the same rotating token, and atomically persist every complete replacement credential set. Production storage needs database transactions, distributed per-connection locking, recovery when persistence fails, multi-instance coordination, and tenant isolation. Make disconnect invalidate continuing refresh access as well as clear local state.
51
65
 
52
66
  If a credential may have been exposed, stop using it, revoke it, rotate the relevant client secret, review logs without copying secret values, and require reauthorization when the grant can no longer be trusted.
53
67
 
@@ -8,6 +8,8 @@ Teal boxes show the normal token lifecycle. The amber box marks a terminal failu
8
8
 
9
9
  ## Determine expiration
10
10
 
11
+ Keep each token set associated with the application user and the authorized Lunch Money user and budgeting account pair. Renewing credentials does not change that pair; selecting another budgeting account requires a new authorization. See [users, budgeting accounts, and access tokens](/oauth/concepts#users-budgeting-accounts-and-access-tokens).
12
+
11
13
  Access tokens are deliberately short-lived. Record when the token response arrives and add its `expires_in` duration to determine the expected expiry. Refresh slightly before that point while accounting for clock skew. Do not parse the opaque access token or assume a fixed lifetime.
12
14
 
13
15
  ## Refresh access
@@ -57,7 +59,7 @@ Do not paste a real client secret or token directly into a command where it may
57
59
 
58
60
  Revocation is different from expiration: expiration ends one credential naturally, while revocation deliberately removes access. Deleting or disabling an application and loss of user access can also make tokens unusable.
59
61
 
60
- A Lunch Money user can also revoke an application's access from the [Connected Apps page](https://my.lunchmoney.app/connected-apps). Your application should treat the resulting authentication failure as terminal and ask the user to authorize again when they want to reconnect.
62
+ A Lunch Money user can also revoke an application's access from the [Connected Apps page](https://my.lunchmoney.app/connected-apps). User revocation applies to the selected budgeting-account connection. Your application should treat the resulting authentication failure as terminal for that connection and ask the user to authorize it again when they want to reconnect. Keep connections to different Lunch Money budgeting accounts for the same application user independent.
61
63
 
62
64
  ## Recovery rules
63
65
 
@@ -84,12 +84,12 @@ If a successful authorization-code exchange does not include a refresh token, co
84
84
  Treat an OAuth `401` as an unknown access-token failure unless a refresh request establishes what happened. When an eligible refresh token is available, attempt one refresh and handle the token endpoint's result, which uses the OAuth format shown above rather than the v2 API format:
85
85
 
86
86
  1. On success, save the replacement tokens and retry the API request once. The access token had merely expired.
87
- 2. On `invalid_grant`, treat the authorization as revoked: mark that user's Lunch Money connection as inactive and discard its tokens. Prompt the user to authorize again the next time they sign in rather than repeatedly interrupting them.
87
+ 2. On `invalid_grant`, treat that connection as revoked: mark the connection for the selected user/account as inactive and discard its tokens. Prompt the user to authorize that connection again the next time they need it rather than repeatedly interrupting them. Leave the user's other budgeting-account connections unchanged.
88
88
  3. On `invalid_client`, stop requests for every user of the client and contact [developer support](mailto:developer-support@lunchmoney.app). Lunch Money may have disabled the client; retrying or asking users to authorize again will not help.
89
89
 
90
90
  If no eligible refresh token is available, discard the rejected access token and ask the user to authorize again.
91
91
 
92
- An OAuth access token can fail because it expired, access was revoked, the client was disabled, the user reauthorized the application, a token was revoked or rotated, or the user lost access to the budgeting account. Every OAuth access-token failure returns the same generic `401` response:
92
+ An OAuth access token can fail because it expired, access was revoked, the client was disabled, the token was revoked or rotated, or the user lost access to the budgeting account. Every OAuth access-token failure returns the same generic `401` response:
93
93
 
94
94
  ```http
95
95
  HTTP/1.1 401
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunch-money/developer-docs",
3
- "version": "2.11.2-preview.2",
3
+ "version": "2.11.2-preview.4",
4
4
  "description": "Developer documentation content for Lunch Money APIs",
5
5
  "exports": {
6
6
  ".": "./package.json",
@@ -7,8 +7,10 @@ The Lunch Money API spec uses a modified version of SEMVER for its versioning me
7
7
 
8
8
  ## v2.11.2 - TBD
9
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
10
+ - Document and enforce required OAuth scopes for every v2 operation and publish the scope and scope-group catalog mapped to V2 `operationId` values.
11
+ - Added a new `403` `insufficient_scope` response
11
12
  - Document the OAuth 2.0 authorization-code flow used to obtain access tokens
13
+ - Add `include_files` query parameter to `GET /transactions/{id}`
12
14
 
13
15
  ## v2.11.1 - TBD
14
16
  - Add `GET /me/account/settings` and `PUT /me/account/settings` for account-level settings
@@ -23,10 +23,10 @@
23
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
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
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>
26
+ <path class="flow lm-flow" d="M950 380 H570"/><rect class="step-bg" x="545" y="358" width="430" height="28" rx="6"/><text class="step" x="760" y="377" text-anchor="middle">5. Present budgeting-account selector and permissions</text>
27
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
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
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
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>
31
+ <path class="flow app-flow" d="M170 710 H940"/><rect class="step-bg" x="400" y="688" width="330" height="28" rx="6"/><text class="step" x="565" y="707" text-anchor="middle">10. Call the V2 API for that user and account</text>
32
32
  </svg>
package/v2/spec/AGENTS.md CHANGED
@@ -41,6 +41,12 @@ Follow these rules whenever editing `v2/spec/lunch-money-api-v2.yaml`:
41
41
  - Add links when they materially help consumers discover a related endpoint or relevant guide.
42
42
  - Do not add links that merely restate obvious navigation.
43
43
 
44
+ ### YAML formatting
45
+
46
+ - Write each description paragraph as a single line. Never hard-wrap prose to a column: inside a folded scalar a line break becomes a space, so a wrap landing beside punctuation or markup silently corrupts the rendered text.
47
+ - Use a folded block (`>-`) only when a description has more than one paragraph, separated by a blank line. A single-paragraph description stays on the key line.
48
+ - Never use a literal block (`|`, `|-`) for prose — newlines become significant, so reflowing later changes the output.
49
+
44
50
  ### Examples
45
51
 
46
52
  Examples are selective documentation aids, not a required artifact for every spec change.