@lunch-money/developer-docs 2.11.2-preview.9 → 2.11.3-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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  Native apps are public clients: they cannot keep a client secret. Use authorization code with PKCE and launch Lunch Money sign-in and consent in a trusted platform authentication browser—not an embedded WebView.
4
4
 
5
+ Lunch Money requires PKCE with the `S256` challenge method for every client, public or confidential. Native clients send no client secret.
6
+
5
7
  ## Recommended libraries
6
8
 
7
9
  - iOS and macOS: [`AppAuth-iOS`](https://github.com/openid/AppAuth-iOS), presented through `ASWebAuthenticationSession`.
@@ -13,23 +15,32 @@ Use Lunch Money's authorization-server discovery document and configure `token_e
13
15
 
14
16
  ## Redirects
15
17
 
16
- Use a redirect mechanism that returns control to your app and that your platform can bind to it. Claimed Universal Links or Android App Links provide stronger app ownership than a custom URI scheme when correctly configured. A private-use scheme must contain a dot, such as `app.example.demo:/oauth/callback`, and must be protected against interception.
18
+ Use a redirect mechanism that returns control to your app and that your platform can bind to it. Claimed HTTPS redirects or Android App Links provide stronger app ownership than a custom URI scheme when correctly configured. A private-use scheme must contain a dot, such as `app.example.demo:/oauth/callback`, and must be protected against interception.
19
+
20
+ Lunch Money accepts a well-formed non-loopback HTTPS redirect URI but does not fetch or validate an Apple `apple-app-site-association` file or Android `assetlinks.json` file. Platform association remains your responsibility:
21
+
22
+ - For AppAuth-iOS HTTPS redirects, use iOS 17.4 or later, add an Associated Domains entitlement for the redirect host with the `webcredentials` service, and publish an `apple-app-site-association` file that lists the app under `webcredentials`. An `applinks` association by itself is not sufficient for AppAuth-iOS.
23
+ - For Android App Links, add an HTTPS intent filter with `android:autoVerify="true"` for the redirect host and publish `https://HOST/.well-known/assetlinks.json` with the app package name and signing-certificate fingerprint.
24
+
25
+ Test the installed application with the production-signed association before release. Successful client registration proves only that the redirect URI is syntactically eligible; it does not prove that the operating system will return the authorization response to your app.
17
26
 
18
27
  Native clients may use an HTTP loopback redirect with `localhost`, `127.0.0.1`, or `[::1]`. The scheme, host, path, and query string must exactly match the registered URI, but the port may differ so the application can bind an ephemeral port at runtime. The exception applies only to loopback redirects for native clients; native HTTPS and private-use scheme redirects must match every component of the registered URI exactly. The loopback hosts are not interchangeable: a client registered with `http://127.0.0.1/cb` cannot authorize with `http://localhost:4000/cb` or `http://[::1]:4000/cb`, so register the host your app actually binds to.
19
28
 
20
29
  ## Store and renew tokens
21
30
 
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.
31
+ Store tokens in Keychain on Apple platforms. If an app and its extensions both refresh, place the complete credential set in a Keychain access group they can share. On Android, use current Keystore-backed storage guidance from your maintained library; do not start new work with deprecated `EncryptedSharedPreferences` APIs.
32
+
33
+ Serialize refreshes with a cross-process lock for each connection. After acquiring the lock, re-read the stored credential set because another process may already have refreshed it. Persist the complete replacement set before using the new access token or releasing the lock. If a successful refresh response is lost before it is saved, do not retry with the consumed refresh token: replay revokes the token family, so the user must authorize again.
23
34
 
24
- After every code exchange, call and strictly validate `GET /v2/me` before adding the credentials to the app's normal connection store. For an app without its own user accounts, identify each local connection by the returned Lunch Money user `id` and budgeting-account `account_id` pair. Atomically create a record for a new pair or replace the credential set for an existing pair. Two Lunch Money users may authorize the same shared `account_id`; their credentials must remain in separate records. If identification or persistence fails, discard the temporary credentials instead of saving a partial or unidentified connection. Retain `budget_name` for display, but never use it as a key because different budgets can share a name.
35
+ After every code exchange, call and strictly validate `GET /v2/me` before adding the credentials to the app's normal connection store. For an app without its own user accounts, identify each local connection by the returned Lunch Money user `id` and budget-account `account_id` pair. Atomically create a record for a new pair or replace the credential set for an existing pair. Two Lunch Money users may authorize the same shared `account_id`; their credentials must remain in separate records. If identification or persistence fails, discard the temporary credentials instead of saving a partial or unidentified connection. Retain `budget_name` for display, but never use it as a key because different budget accounts can share a name.
25
36
 
26
- The platform authentication browser may reuse an existing Lunch Money login, and Lunch Money may let the person use a different user from the authorization screen. An ephemeral-session request is only an intent to avoid shared browser state; it does not prove which user completed authorization. Do not infer identity from browser cookies, callback fields, budget names, or token contents. The validated `/v2/me` response is authoritative.
37
+ The platform authentication browser may reuse an existing Lunch Money login, and Lunch Money may let the person use a different user from the authorization screen. An ephemeral-session request is only an intent to avoid shared browser state; it does not prove which user completed authorization. Do not infer identity from browser cookies, callback fields, budget account names, or token contents. The validated `/v2/me` response is authoritative.
27
38
 
28
- Show the active Lunch Money identity when the app may retain more than one, show which authorized budget is active, and provide one separate **Authorize another budget** action that starts OAuth. Do not require another client-side action for switching Lunch Money users. Selecting an existing connection must not start authorization again, and the app must allow selection only among saved budgets for the active Lunch Money user.
39
+ Show the active Lunch Money identity when the app may retain more than one, show which authorized budget account is active, and provide one separate **Authorize another budget account** action that starts OAuth. Do not require another client-side action for switching Lunch Money users. Selecting an existing connection must not start authorization again, and the app must allow selection only among saved budget accounts for the active Lunch Money user.
29
40
 
30
- All API calls, refreshes, revocations, local deletions, and cached data must apply to the active user/account connection. Clear or partition profile, configuration, and cached financial data when the identity or budget changes. When the active connection is removed, choose a deterministic remaining connection for that Lunch Money user or enter a disconnected state; never expose another user's hidden connection as a fallback. Use a secondary label such as the account ID only when duplicate visible budget names need disambiguation.
41
+ All API calls, refreshes, revocations, local deletions, and cached data must apply to the active user/account connection. Clear or partition profile, configuration, and cached financial data when the identity or budget account changes. When the active connection is removed, choose a deterministic remaining connection for that Lunch Money user or enter a disconnected state; never expose another user's hidden connection as a fallback. Use a secondary label such as the account ID only when duplicate visible budget account names need disambiguation.
31
42
 
32
- The app may retain credentials for inactive Lunch Money users in secure storage. Switching users hides the previous identity's connections without revoking or deleting them, and authorizing as that identity later may restore its saved budgets. Alternatively, an app may delete credentials during its own sign-out or user-switch lifecycle. Make the chosen behavior understandable without prescribing a particular native control.
43
+ The app may retain credentials for inactive Lunch Money users in secure storage. Switching users hides the previous identity's connections without revoking or deleting them, and authorizing as that identity later may restore its saved budget accounts. Alternatively, an app may delete credentials during its own sign-out or user-switch lifecycle. Make the chosen behavior understandable without prescribing a particular native control.
33
44
 
34
45
  Present remote disconnect or revocation separately from forgetting a credential only on the device. Confirm local-only deletion and explain that it leaves the remote authorization active.
35
46
 
@@ -39,12 +50,40 @@ A native app can support multiple application users on the same device. If your
39
50
 
40
51
  On application sign-out or application-user switching, stop work using the previous application 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 application owner signs in again. Do not attach an OAuth callback or refresh result from the previous session to the newly signed-in application user.
41
52
 
42
- 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.
53
+ 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 budget-account records alone do not authenticate different people.
43
54
 
44
55
  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.
45
56
 
46
57
  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.
47
58
 
48
- 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.
59
+ ## AppAuth-iOS outline
60
+
61
+ Use AppAuth-iOS to discover Lunch Money's authorization-server metadata from `https://api.lunchmoney.dev`, then create an `OIDAuthorizationRequest` with the discovered configuration, your client ID, no client secret, the registered redirect URI, and the authorization-code response type. AppAuth-iOS presents the request with `ASWebAuthenticationSession`, generates an `S256` PKCE challenge, handles the callback, and exchanges the code when you use its authorization-state convenience flow.
62
+
63
+ The following outline is illustrative; adapt ownership, error handling, and persistence to your application:
64
+
65
+ ```swift
66
+ let issuer = URL(string: "https://api.lunchmoney.dev")!
67
+
68
+ OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
69
+ guard let configuration else { return handle(error) }
70
+ let request = OIDAuthorizationRequest(
71
+ configuration: configuration,
72
+ clientId: clientID,
73
+ clientSecret: nil,
74
+ scopes: nil,
75
+ redirectURL: redirectURL,
76
+ responseType: OIDResponseTypeCode,
77
+ additionalParameters: nil
78
+ )
79
+ authorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: viewController) { state, error in
80
+ // Temporarily hold state, then validate GET /v2/me before saving it.
81
+ }
82
+ }
83
+ ```
84
+
85
+ After exchange, call `GET /v2/me` with the access token and strictly validate the Lunch Money user and budget-account identity before saving the credential set. For refresh, acquire the connection's cross-process lock, re-read the Keychain access-group record, ask `OIDAuthState` for a fresh access token only if the stored state still needs refresh, save the complete replacement state, and then make the API request. Treat a terminal refresh failure as a request for interactive authorization.
86
+
87
+ This is integration guidance, not a complete Swift sample. The upstream AppAuth-iOS project remains the source for current library APIs and platform setup. Expo developers can use the [native Expo sample](/oauth/sample-applications#native-expo-sample) for a complete Lunch Money walkthrough and reference implementation.
49
88
 
50
89
  Next: [Review OAuth security guidance](/oauth/security).
@@ -1,19 +1,19 @@
1
1
  # Register an OAuth client
2
2
 
3
- Before you add OAuth to your application code, register an OAuth client with Lunch Money. The client identifies your application during authorization and tells Lunch Money who is requesting access, where to return the user, and which permissions the application needs.
3
+ Before you add OAuth to your application code, register an OAuth client with Lunch Money. The client identifies your application during authorization and tells Lunch Money who is requesting access, where to return the user, and which scopes the application needs.
4
4
 
5
5
  [Register a new OAuth client](/oauth/applications/new) in the Developer Portal when you are ready to begin.
6
6
 
7
7
  > [!NOTE] Development access is owner-only
8
8
  > A newly registered client starts in development. Only the Lunch Money user who owns it can authorize it until the client has been reviewed and approved.
9
9
 
10
- ## Plan the permissions
10
+ ## Plan the scopes
11
11
 
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.
12
+ List the Lunch Money features your application will provide, then use the [OAuth scopes](/oauth/scopes) page to choose the smallest complete set of scopes 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
- If your application may support more than one Lunch Money user or budgeting account, always include `me:read`. After authorization, `GET /v2/me` lets your application record both the Lunch Money user's `id` and the selected budgeting account's `account_id`, then store the connection under the complete application-owner and Lunch Money user/account identity. The same scope is also required when your application only needs to identify one connected budgeting account.
14
+ If your application may support more than one Lunch Money user or budget account, always include `me:read`. After authorization, `GET /v2/me` lets your application record both the Lunch Money user's `id` and the selected budget account's `account_id`, then store the connection under the complete application-owner and Lunch Money user/account identity. The same scope is also required when your application only needs to identify one connected budget account.
15
15
 
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.
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 scopes—settled before you submit the client for review.
17
17
 
18
18
  If functionality added after launch needs a scope that is not already registered, you must register a replacement client with the new complete scope set and have it reviewed before rollout. Existing authorizations do not transfer to the replacement client, so users must authorize it before they can use the new functionality. Treat that reauthorization as part of the feature launch: explain what the application can now do with their Lunch Money data and invite them to authorize the replacement client when they want to enable it.
19
19
 
@@ -30,7 +30,7 @@ The client type cannot be changed after registration. See [OAuth concepts](/oaut
30
30
 
31
31
  A redirect URI tells Lunch Money where to return the user after authorization. Register every callback address your application will use. Confidential web clients must match every component of a registered URI, including its scheme, host, port, path, and query string. Native-client loopback redirects use the same exact matching except that the ephemeral port is ignored.
32
32
 
33
- Web applications normally use an HTTPS callback. If you need a callback for local development, review the [loopback guidance](/oauth/development#local-loopback-callbacks) before registering one. Mobile and desktop applications may use a private-use scheme containing a dot, such as `app.example.demo:/oauth/callback`, or a verified HTTPS link accepted by the Developer Portal.
33
+ Web applications normally use an HTTPS callback. If you need a callback for local development, review the [loopback guidance](/oauth/development#local-loopback-callbacks) before registering one. Mobile and desktop applications may use a private-use scheme containing a dot, such as `app.example.demo:/oauth/callback`, or a non-loopback HTTPS link. Lunch Money checks that the HTTPS URI is well formed but does not verify an Apple or Android association file when you register it. Configure the URI as a claimed HTTPS redirect on the target platform before relying on it; see the [native redirect guidance](/oauth/native-apps#redirects).
34
34
 
35
35
  After the client is approved, you can add or replace redirect URIs in the Developer Portal without submitting another initial review. An approved confidential web client must keep a production HTTPS callback. See [after approval](/oauth/review-and-approval#after-approval).
36
36
 
@@ -49,6 +49,8 @@ Open the [new OAuth client form](/oauth/applications/new) in the Developer Porta
49
49
 
50
50
  You can own up to 25 non-deleted OAuth clients. If you reach the limit, delete a client you no longer use before registering another.
51
51
 
52
+ The support email must be confirmed before you can submit the client for review. If it matches your own verified Lunch Money account email, Lunch Money confirms it automatically. For any other address—including one used by another Lunch Money account—Lunch Money sends a confirmation link from `support@lunchmoney.app` to that mailbox. The link is valid for 72 hours, so use an address you can access and follow the link promptly.
53
+
52
54
  After registration, record the client ID in your application configuration. A client ID identifies the registration but is not a secret.
53
55
 
54
56
  If you registered a **Confidential web client**, create a client secret from its details page. Lunch Money shows the secret value only once. Copy it immediately into a secret manager or protected server configuration; it cannot be retrieved later. Never commit it or expose it in browser or mobile code.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Every new OAuth client starts in development, where only its owner can authorize it. This gives you time to build and test the application without making an unfinished integration available to other Lunch Money users.
4
4
 
5
- When the application is ready, submit its client for review in the Developer Portal. A Lunch Money employee reviews the application information, requested permissions, redirect configuration, and other readiness requirements. Approval moves the client to `active`, allowing other Lunch Money users to authorize the application.
5
+ When the application is ready, submit its client for review in the Developer Portal. A Lunch Money employee reviews the application information, requested scopes, redirect configuration, and other readiness requirements. Approval moves the client to `active`, allowing other Lunch Money users to authorize the application.
6
6
 
7
7
  ## Lifecycle states
8
8
 
@@ -23,28 +23,33 @@ Submitting a client for review is how you make your application authorizable by
23
23
  Before requesting review, make sure the client has:
24
24
 
25
25
  - an application description, developer name, and logo so users can identify the application and who created it;
26
- - a monitored support email where users and Lunch Money can reach you;
26
+ - a confirmed, monitored support email where users and Lunch Money can reach you;
27
27
  - a publicly accessible homepage URL where users can learn about the application;
28
28
  - a publicly accessible privacy policy URL explaining how the application handles user data;
29
29
  - at least one eligible redirect URI—a confidential web client needs a non-loopback production HTTPS redirect, while a native client may use a private-use scheme or loopback redirect;
30
- - a recognized, supported scope set and an explanation of why the application needs each permission; and
30
+ - a recognized, supported scope set and an explanation of why the application needs each scope; and
31
31
  - acceptance of the current Lunch Money API Terms of Use.
32
32
 
33
33
  The privacy policy must be available at a direct, stable public URL and explain how the application collects, uses, stores, shares, and deletes user data. A published privacy-policy page in a public source-code repository can satisfy this requirement. An in-app page that requires installation or sign-in, or a link to source code without a clear privacy policy, does not.
34
34
 
35
- Fully test the client before submission. A confidential web client also needs at least one active, non-expired secret; a native or public client does not.
35
+ Fully test the client before submission. Testing by the client owner is sufficient; submission does not require beta-tester coverage. A confidential web client also needs at least one active, non-expired secret; a native or public client does not.
36
36
 
37
- Explain which features use each requested permission, how that access benefits users, and why narrower scopes are insufficient. If the client includes `offline_access`, explain why the integration must operate while the user is absent. You may also provide documentation, source-code, and demonstration URLs or context about changes made after earlier feedback.
37
+ Explain which features use each requested scope, how that access benefits users, and why narrower scopes are insufficient. If the client includes `offline_access`, explain why the integration must operate while the user is absent. You may also provide documentation, source-code, and demonstration URLs or context about changes made after earlier feedback.
38
38
 
39
- Open-source applications are welcome. If your source code is public, include the repository URL in the review request's additional context. It can help the reviewer understand how your application uses Lunch Money data and the permissions it requests. Publishing source code is optional and is not required for approval.
39
+ Open-source applications are welcome. If your source code is public, include the repository URL in the review request's additional context. It can help the reviewer understand how your application uses Lunch Money data and the scopes it requests. Publishing source code is optional and is not required for approval.
40
40
 
41
41
  A confidential web client may retain owner-only loopback callbacks alongside its production HTTPS callback, but it still needs a non-loopback production HTTPS redirect before review.
42
42
 
43
+ Confirm the support email from the mailbox before you submit the client. The Developer Portal's readiness checklist shows whether it is confirmed and prevents submission until it is. If you change the address before approval, Lunch Money clears its confirmation; confirm the new address before the client can be submitted or approved.
44
+
43
45
  ### Native applications
44
46
 
45
47
  You do not need to create a standalone marketing website for a native application. Its public Apple App Store or Google Play listing can serve as the homepage if it clearly explains what the application does and identifies the developer. A public pre-release listing, project page, or README in a public source-code repository can also work.
46
48
 
47
- If you provide a pre-release listing or beta-testing link, use the review request's additional context to explain how to access it. Include any required opt-in steps, supported platforms or devices, and other information the reviewer needs to evaluate the application. Do not include passwords, client secrets, tokens, or other credentials.
49
+ Lunch Money can approve a native client before its public App Store or Google Play release when the reviewer can access a TestFlight, other beta, or pre-release build. Use the review request's additional context to explain how to access it. Include any required opt-in steps, supported platforms or devices, and other information the reviewer needs to evaluate the application. Do not include passwords, client secrets, tokens, or other credentials.
50
+
51
+ > [!NOTE] Prepare for Apple App Review
52
+ > If the iOS app requires Lunch Money authorization, provide Apple with a dedicated test Lunch Money account in the App Store Connect review information rather than a personal account. Make sure the account and its test budget account remain available throughout review. Two-factor authentication can prevent the reviewer from signing in unless you also provide a usable authentication code or another review-ready access path in the review notes.
48
53
 
49
54
  Native clients may be reviewed with loopback or private-use scheme redirects.
50
55
 
@@ -68,9 +73,11 @@ There is no review comment thread, so use the new request's additional context t
68
73
 
69
74
  An `active` client can be authorized by other users. Keep its configuration current in the Developer Portal. You do not need to contact Lunch Money to rename the application, move hosting, or update a support address.
70
75
 
71
- Changes save immediately, and the client stays `active`. Redirect URIs, support email, description, and developer name take effect as soon as you save them. An approved confidential web client must keep at least one public HTTPS redirect.
76
+ Changes save immediately, and the client stays `active`. Redirect URIs, description, and developer name take effect as soon as you save them. An approved confidential web client must keep at least one public HTTPS redirect.
77
+
78
+ Name, logo, support email, homepage URL, and privacy policy URL also save immediately, but Lunch Money users continue to see the last approved identity on the consent screen and Connected Apps page until Lunch Money reviews the update. The Developer Portal shows that as an identity update in review; it is not a return to `rejected`, and existing authorizations keep working.
72
79
 
73
- Name, logo, homepage URL, and privacy policy URL also save immediately, but Lunch Money users continue to see the last approved identity on the consent screen and Connected Apps page until Lunch Money reviews the update. The Developer Portal shows that as an identity update in review; it is not a return to `rejected`, and existing authorizations keep working.
80
+ When you change the support email, confirm the new address before Lunch Money can approve the identity update. Until it is confirmed and approved, users continue to see the previous approved address, and Lunch Money does not send notifications to the new address. Lunch Money tells the previously confirmed support address that the client was changed.
74
81
 
75
82
  Client-secret rotation and revocation remain available subject to lifecycle and security checks. Scopes and client type cannot be changed after registration. A scope change requires a replacement client and user reauthorization.
76
83
 
@@ -179,7 +179,7 @@ The request should fail because Lunch Money no longer accepts the stored access
179
179
 
180
180
  ### Local reset is not revocation
181
181
 
182
- **Forget local credential only** clears the active budget's locally stored credential after confirmation. It does not contact Lunch Money and does not revoke active access.
182
+ **Forget local credential only** clears the active budget account's locally stored credential after confirmation. It does not contact Lunch Money and does not revoke active access.
183
183
 
184
184
  When access is still active, revoke it first through **Disconnect active budget** or Lunch Money's Connected Apps page. Forget the local credential by itself only when the remote authorization has already been revoked or you deliberately want to clear this disposable local demonstration.
185
185
 
@@ -202,7 +202,7 @@ Before adapting the code, read the repository's [OAuth code guide](https://githu
202
202
 
203
203
  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.
204
204
 
205
- The sample represents an authenticated application user with a fixed local demo identity. After authorization, it uses the Lunch Money user `id` returned by `GET /v2/me` to choose which saved budgets are visible; credentials retained for another Lunch Money user remain hidden. A production confidential application replaces the fixed demo identity with the application user from its authenticated server-side session and identifies each owned connection by the returned Lunch Money user `id` and budgeting-account `account_id` pair.
205
+ The sample represents an authenticated application user with a fixed local demo identity. After authorization, it uses the Lunch Money user `id` returned by `GET /v2/me` to choose which saved budget accounts are visible; credentials retained for another Lunch Money user remain hidden. A production confidential application replaces the fixed demo identity with the application user from its authenticated server-side session and identifies each owned connection by the returned Lunch Money user `id` and budget-account `account_id` pair.
206
206
 
207
207
  Treat these modules as a working reference, not as a package to copy unchanged into production. The in-memory implementation demonstrates serialized refresh and complete credential replacement for one `applicationUserId` and `connectionId`, but it does not provide durable encrypted storage, database transactions, distributed per-connection locking, failed-persistence recovery across restarts, multi-instance coordination, or tenant isolation. The sample keeps those application-specific decisions outside the teaching code so you can see exactly where your implementation must provide them.
208
208
 
@@ -230,9 +230,9 @@ Node.js sample above instead.
230
230
 
231
231
  Unlike the confidential sample, the native sample has no separate application
232
232
  account or server-side application-user boundary. It identifies local
233
- connections directly by the Lunch Money user `id` and budgeting-account
233
+ connections directly by the Lunch Money user `id` and budget-account
234
234
  `account_id` returned by `GET /v2/me`. It can retain connections for more than
235
- one Lunch Money user on the device, but only the active user's budgets are
235
+ one Lunch Money user on the device, but only the active user's budget accounts are
236
236
  visible or selectable. Applications that add their own sign-in system must put
237
237
  that pair inside the authenticated application user's storage and ownership
238
238
  boundary.
@@ -1,13 +1,13 @@
1
1
  # OAuth scopes
2
2
 
3
- Choose the smallest complete set of permissions your application needs. This page is generated from the published `@lunch-money/v2-api-spec/oauth-scopes` catalog; do not edit it by hand.
3
+ Scopes are named permissions that define what an OAuth client may do with Lunch Money data. Choose the smallest complete set of scopes your application needs. This page is generated from the published `@lunch-money/v2-api-spec/oauth-scopes` catalog; do not edit it by hand.
4
4
 
5
5
  > [!NOTE] Scopes are fixed when you create a client
6
- > Authorization requests omit `scope`. To change permissions later, create and review a replacement client, update your integration, and ask every user to authorize it.
6
+ > Authorization requests omit `scope`. To change scopes later, create and review a replacement client, update your integration, and ask every user to authorize it.
7
7
 
8
8
  ## Scope groups
9
9
 
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.
10
+ Groups are selection shortcuts, not additional scopes. Every predefined group includes `me:read` so an application can identify the authorized Lunch Money user and budget account. A group expands to the exact scopes listed below.
11
11
 
12
12
  ### Read only
13
13
 
@@ -20,13 +20,13 @@ Keeping access tokens on the server reduces their exposure to browser code. Usin
20
20
 
21
21
  In the browser-based model, the short-lived access token is also visible to the signed-in user in the request's `Authorization` header. That visibility is expected and is not, by itself, a credential leak. The tradeoff is that a malicious script running on the page—or someone with access to that browser session—could copy the bearer token and use it until it expires or is revoked.
22
22
 
23
- If your browser code uses access tokens, keep them available for no longer than necessary, avoid persistent browser storage when practical, and protect the application against cross-site scripting. Regardless of the architecture, keep the client secret and refresh tokens on the server, never put bearer tokens in URLs or logs, and associate each authorization with the correct application user and Lunch Money budgeting account.
23
+ If your browser code uses access tokens, keep them available for no longer than necessary, avoid persistent browser storage when practical, and protect the application against cross-site scripting. Regardless of the architecture, keep the client secret and refresh tokens on the server, never put bearer tokens in URLs or logs, and associate each authorization with the correct application user and Lunch Money budget account.
24
24
 
25
25
  ## Associate credentials with your application user
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 each budgeting-account connection under a stable internal application-user ID. Within that owner, use both the Lunch Money user `id` and budgeting-account `account_id` returned by `GET /v2/me` as the provider identity. Your database may assign a separate internal connection ID for routing and references; it does not replace the `(application user ID, Lunch Money user id, account_id)` fields used to create or update a provider connection. Do not use an email address, callback parameter, form field, browser cookie, or other browser-supplied value to establish the credential owner or provider identity.
29
+ Store each budget-account connection under a stable internal application-user ID. Within that owner, use both the Lunch Money user `id` and budget-account `account_id` returned by `GET /v2/me` as the provider identity. Your database may assign a separate internal connection ID for routing and references; it does not replace the `(application user ID, Lunch Money user id, account_id)` fields used to create or update a provider connection. Do not use an email address, callback parameter, form field, browser cookie, or other browser-supplied value to establish the credential owner or provider identity.
30
30
 
31
31
  An application user may own connections for more than one Lunch Money user, although applications do not have to expose that capability. If your application supports only one Lunch Money identity per application user, detect a different `id` after `GET /v2/me` and handle it explicitly; never overwrite the existing identity's credentials.
32
32
 
@@ -38,13 +38,13 @@ Authenticated application user
38
38
 
39
39
  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.
40
40
 
41
- 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.
41
+ 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 budget account. Each application user must authorize their own connection. If Alex has authorized two budget 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.
42
42
 
43
43
  Enforce this ownership and identity 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 match both the returned Lunch Money `id` and `account_id`. A different Lunch Money user authorizing the same shared account must not silently overwrite an existing connection.
44
44
 
45
45
  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.
46
46
 
47
- Apply API calls, refresh, revocation, local deletion, and caches only to the active Lunch Money user and budgeting-account connection. Disconnecting a budgeting account should revoke continuing access for that connection and remove its locally stored credentials while keeping the active user's other connections intact. If the active user's final connection is removed, enter a disconnected state rather than falling back to another Lunch Money user's hidden connection.
47
+ Apply API calls, refresh, revocation, local deletion, and caches only to the active Lunch Money user and budget-account connection. Disconnecting a budget account should revoke continuing access for that connection and remove its locally stored credentials while keeping the active user's other connections intact. If the active user's final connection is removed, enter a disconnected state rather than falling back to another Lunch Money user's hidden connection.
48
48
 
49
49
  On application sign-out or Lunch Money user switching, clear session-specific selections and displayed or cached data so the newly active identity cannot see the previous identity's information. An application may retain the previous Lunch Money user's encrypted credentials when its ownership model permits it, but those connections must remain hidden and unavailable until that identity becomes active again. It may instead delete credentials during its own sign-out or switching lifecycle; that is an application policy.
50
50
 
@@ -65,6 +65,10 @@ The [confidential Node.js sample](/oauth/sample-applications#understand-the-conf
65
65
 
66
66
  Choose only the resource scopes needed for expected features. Add `offline_access` only for genuine unattended operation. Remember that scope changes require a replacement client and reauthorization; they cannot be requested dynamically.
67
67
 
68
+ ## Keep the support address confirmed
69
+
70
+ Use a monitored support mailbox that your team controls, and keep it current in the Developer Portal. Lunch Money sends client notifications to a support address only after someone with access to that mailbox confirms it. Changing the address clears its confirmation, so follow the new confirmation link before relying on that mailbox for notifications.
71
+
68
72
  ## Operate credentials safely
69
73
 
70
74
  Rotate client secrets on a schedule appropriate for your application's risk and immediately when one may have been exposed. A client can have up to three non-revoked secrets, so confirm that it has room before starting a rotation. Expired secrets still count toward this limit until you revoke them. 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.
@@ -8,7 +8,7 @@ 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).
11
+ Keep each token set associated with the application user and the authorized Lunch Money user and budget account pair. Renewing credentials does not change that pair; selecting another budget account requires a new authorization. See [users, budget accounts, and access tokens](/oauth/concepts#users-budget-accounts-and-access-tokens).
12
12
 
13
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.
14
14
 
@@ -18,18 +18,25 @@ A refresh token is issued after a successful authorization-code exchange only wh
18
18
 
19
19
  The [confidential Node.js sample](/oauth/sample-applications#try-oauth-with-your-lunch-money-account) keeps refresh optional within its first-run walkthrough. Register the sample client with `offline_access` to exercise the refresh branch, or omit it to focus on authorization, `GET /v2/me`, revocation, and reauthorization.
20
20
 
21
- Send a form-encoded `grant_type=refresh_token` request to the discovered token endpoint. Confidential clients authenticate with `client_secret_basic`; public clients send their client ID without a secret. A refresh never expands or switches the grant's registered scopes, Lunch Money user, or budgeting-account identity.
21
+ Send a form-encoded `grant_type=refresh_token` request to the discovered token endpoint. Confidential clients authenticate with `client_secret_basic`; public clients send their client ID without a secret. A refresh never expands or switches the grant's registered scopes, Lunch Money user, or budget-account identity.
22
22
 
23
- Atomically persist the complete replacement token set before discarding the previous value, and serialize refreshes for each active application-user and Lunch Money user/account connection. Lunch Money rotates refresh tokens; replay of a consumed token can invalidate the token family. Treat transient failures according to a bounded retry policy, but treat `invalid_grant` as terminal: delete unusable credentials for that connection and start a fresh authorization.
23
+ Atomically persist the complete replacement token set before discarding the previous value, and serialize refreshes for each active application-user and Lunch Money user/account connection. When more than one process can refresh the same connection, use a shared credential store and a cross-process lock. After acquiring the lock, re-read the stored refresh token because another process may already have replaced it. Persist the complete replacement set before using the new access token or releasing the lock.
24
+
25
+ Lunch Money rotates refresh tokens on every successful refresh. A consumed token cannot be retried: replay revokes the token family. If the response to a successful refresh is lost before the replacement set is saved, require the user to authorize again rather than retrying the previous token. Treat other transient failures according to a bounded retry policy, but treat `invalid_grant` as terminal: delete unusable credentials for that connection and start a fresh authorization.
24
26
 
25
27
  ## Revoke access
26
28
 
27
- Send the active connection's token to the discovered revocation endpoint using the authentication method appropriate to the client. Revocation returns success without revealing unnecessary token state. Locally discard that connection's access token, refresh token, and related session state even if the network response is ambiguous. Keep other user/account connections unchanged.
29
+ To invalidate a complete authorization, send the active connection's current refresh token to `POST /oauth/revoke` using the authentication method appropriate to the client. This revokes the grant and every access token and refresh token in its token family. Locally discard that connection's token set and related session state even if the network response is ambiguous. Keep other user/account connections unchanged, including connections to another budget account for the same Lunch Money user.
28
30
 
29
31
  > [!TIP] Revocation endpoint
30
32
  > The currently resolved endpoint is `https://api.lunchmoney.dev/oauth/revoke`. Use authorization-server discovery to configure deployed applications so they receive the current endpoint automatically.
31
33
 
32
- For a quick development test, the following requests revoke an access token. They assume the values have already been loaded into shell variables from your secure development configuration.
34
+ > [!WARNING] Revoke before replacement authorization
35
+ > Reauthorization may reuse the existing grant. Revoke its current refresh token before starting replacement authorization. Revoking the old refresh token after the new exchange also revokes the credentials returned by that exchange.
36
+
37
+ Revoke the complete authorization only when you must invalidate every copy of its credentials—for example, after suspected credential exposure or when signing every installation or shared-backend session out. For a normal reconnection, keep the grant and atomically replace the connection's locally stored token set with the successful exchange response.
38
+
39
+ The following requests revoke the complete authorization. They assume the current refresh token and client credentials have already been loaded into shell variables from your secure development configuration.
33
40
 
34
41
  :::tabs
35
42
 
@@ -39,8 +46,8 @@ For a quick development test, the following requests revoke an access token. The
39
46
  curl --request POST 'https://api.lunchmoney.dev/oauth/revoke' \
40
47
  --user "$LUNCH_MONEY_CLIENT_ID:$LUNCH_MONEY_CLIENT_SECRET" \
41
48
  --header 'Content-Type: application/x-www-form-urlencoded' \
42
- --data-urlencode "token=$LUNCH_MONEY_ACCESS_TOKEN" \
43
- --data 'token_type_hint=access_token'
49
+ --data-urlencode "token=$LUNCH_MONEY_REFRESH_TOKEN" \
50
+ --data 'token_type_hint=refresh_token'
44
51
  ```
45
52
 
46
53
  @tab Native or public client
@@ -49,17 +56,19 @@ curl --request POST 'https://api.lunchmoney.dev/oauth/revoke' \
49
56
  curl --request POST 'https://api.lunchmoney.dev/oauth/revoke' \
50
57
  --header 'Content-Type: application/x-www-form-urlencoded' \
51
58
  --data-urlencode "client_id=$LUNCH_MONEY_CLIENT_ID" \
52
- --data-urlencode "token=$LUNCH_MONEY_ACCESS_TOKEN" \
53
- --data 'token_type_hint=access_token'
59
+ --data-urlencode "token=$LUNCH_MONEY_REFRESH_TOKEN" \
60
+ --data 'token_type_hint=refresh_token'
54
61
  ```
55
62
 
56
63
  :::
57
64
 
58
- Do not paste a real client secret or token directly into a command where it may be saved in shell history. If the authorization also has a refresh token, revoke or otherwise invalidate it before testing a flow that should require the user to authorize again, then discard both tokens locally.
65
+ The optional `token_type_hint` may be `access_token` or `refresh_token`. To revoke only the current access token, send that token as `token` with `token_type_hint=access_token`. An active refresh token remains usable and can obtain a replacement access token.
66
+
67
+ Do not paste a real client secret or token directly into a command where it may be saved in shell history. After revoking the complete authorization, discard both tokens locally.
59
68
 
60
69
  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.
61
70
 
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 Lunch Money user and budgeting-account authorization. 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 every stored Lunch Money user/account connection independent, including when two users have authorized the same shared `account_id`.
71
+ An application that no longer has the current refresh token cannot revoke the complete authorization. Direct the Lunch Money user to the [Connected Apps page](https://my.lunchmoney.app/connected-apps) to disconnect it. User revocation applies to the selected Lunch Money user and budget-account authorization. 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 every stored Lunch Money user/account connection independent, including when two users have authorized the same shared `account_id`.
63
72
 
64
73
  Remote revocation is different from local credential deletion. If an application offers a local-only “forget” action, confirm it and explain that the remote authorization remains active. Removing the active user's final local connection should leave the application disconnected rather than expose a hidden connection belonging to another Lunch Money user.
65
74
 
@@ -17,13 +17,13 @@ For a runnable comparison, [try OAuth with the confidential Node.js sample](/oau
17
17
 
18
18
  Authorization can fail before your application reaches the token endpoint. Lunch Money may return a lower-case OAuth error such as `invalid_request`, `invalid_scope`, or `access_denied`.
19
19
 
20
- When Lunch Money can safely use the registered callback, it redirects the user's browser with an `error`, an optional `error_description`, and the original `state` as query parameters:
20
+ When Lunch Money can safely use the registered callback, it redirects the user's browser with an `error`, an optional `error_description`, the original `state`, and the authorization-server issuer `iss` as query parameters:
21
21
 
22
22
  ```text
23
- https://example.com/oauth/callback?error=access_denied&state=RETURNED_STATE
23
+ https://example.com/oauth/callback?error=access_denied&state=RETURNED_STATE&iss=https%3A%2F%2Fapi.lunchmoney.dev
24
24
  ```
25
25
 
26
- Match on `error` when it is returned, verify `state`, and treat `error_description` as human-readable text that may change.
26
+ Match on `error` when it is returned, verify `state`, and treat `error_description` as human-readable text that may change. Decode `iss` and compare it with the `issuer` value from authorization-server metadata using exact string comparison. Reject the callback if `iss` is missing or does not match; do not exchange a returned code.
27
27
 
28
28
  | OAuth error | Likely cause | What to do |
29
29
  | --- | --- | --- |
@@ -35,7 +35,7 @@ Match on `error` when it is returned, verify `state`, and treat `error_descripti
35
35
 
36
36
  Until a client is approved, only its owner may authorize the application. Anyone else is stopped at the consent screen and is never returned to your application, so you will see no callback and no `error` parameter for these attempts.
37
37
 
38
- Those users are shown the support email registered with your client so they can reach you. If you did not expect authorization to be unavailable, check the client in the [Developer Portal](/oauth/applications). A client stays in development until it is submitted for review and approved. A rejected or disabled client also prevents other users from authorizing the application.
38
+ Those users are shown the support email registered with your client so they can reach you. For an active client with a support-email update under review, Lunch Money shows the last approved address rather than the newly saved one. If you did not expect authorization to be unavailable, check the client in the [Developer Portal](/oauth/applications). A client stays in development until it is submitted for review and approved. A rejected or disabled client also prevents other users from authorizing the application.
39
39
 
40
40
  ### Redirect URI problems
41
41
 
@@ -48,11 +48,11 @@ The page displays one of two codes, a UTC timestamp, and the application name an
48
48
  | `redirect_uri_mismatch` | The address your application sent is not registered for the client, or differs from a registered one | Open the client in the [Developer Portal](/oauth/applications) and compare the address with its registered redirect URIs. Web-client redirects and native HTTPS or private-use scheme redirects must match every component exactly. For a native HTTP loopback redirect, the scheme, host, path, and query string must match, but the ephemeral port may differ. See [local callback failures](#local-callback-failures). |
49
49
  | `redirect_uri_missing` | No `redirect_uri` was sent, and the client has more than one registered, so Lunch Money cannot choose | Send the parameter explicitly. A client with exactly one registered address may omit it; Lunch Money then uses the registered value. |
50
50
 
51
- The page offers the user a prefilled message addressed to the support email registered with your client. It contains the error code, the timestamp, the application name and client ID, and the callback address your application sent, reduced to its origin and path with any query string removed. Compare that sanitized callback address with the redirect URIs registered for the client, then correct either the registered URI or the address your application sends.
51
+ The page offers the user a prefilled message addressed to the support email registered with your client. For an active client with a support-email update under review, this is the last approved address, which can differ from the address currently saved in the Developer Portal. The message contains the error code, the timestamp, the application name and client ID, and the callback address your application sent, reduced to its origin and path with any query string removed. Compare that sanitized callback address with the redirect URIs registered for the client, then correct either the registered URI or the address your application sends.
52
52
 
53
53
  ## Callback validation failures
54
54
 
55
- After Lunch Money returns the browser to the registered callback, your application must verify that the returned `state` matches the authorization attempt. A missing, changed, or reused value is a state mismatch detected by your application, not an error reported by Lunch Money. Stop the flow, discard the authorization code, clear the one-time state, and begin again. Never exchange the code.
55
+ After Lunch Money returns the browser to the registered callback, your application must verify that the returned `state` matches the authorization attempt and that `iss` exactly matches the discovered issuer. A missing, changed, or reused `state`, or a missing or mismatched `iss`, is a callback validation failure detected by your application, not an error reported by Lunch Money. Stop the flow, discard the authorization code, clear the one-time state, and begin again. Never exchange the code.
56
56
 
57
57
  Your application should show the user a safe error message and enough internal reference information for your team to find the failed attempt without exposing the callback URL or its parameters.
58
58
 
@@ -71,9 +71,15 @@ Match on `error`. Treat `error_description` as human-readable text that may chan
71
71
 
72
72
  | `error` value | Likely cause | What to do |
73
73
  | --- | --- | --- |
74
- | `invalid_client` | Client authentication is missing or wrong, or Lunch Money disabled the client | Confirm that a confidential client uses HTTP Basic with an active secret and that a public native client sends its client ID without a secret. If previously working credentials are still configured correctly, stop requests and contact [developer support](mailto:developer-support@lunchmoney.app). Reauthorizing users will not restore a disabled client. |
74
+ | `invalid_request` | The request is incomplete or malformed, a parameter is duplicated, client authentication is missing or supplied more than once, or the requested grant is not enabled for this client | Do not retry the unchanged request. Confirm that the body is form encoded, each required parameter appears exactly once, and the client uses its registered authentication method. Confidential clients use HTTP Basic; public clients send `client_id` without a secret. |
75
+ | `invalid_client` | The client ID is unknown, the secret is wrong or expired, the authentication method does not match the registered client type, or Lunch Money disabled or deleted the client | Confirm the client ID, client type, and authentication method. For a confidential client, confirm that HTTP Basic uses an active secret. If previously working credentials are still configured correctly, stop requests and contact [developer support](mailto:developer-support@lunchmoney.app). Reauthorizing users will not restore a disabled client. |
75
76
  | `invalid_grant` during code exchange | The code expired, was already used, has the wrong PKCE verifier, or does not match the redirect/client | Start a new authorization. Do not retry the same code. |
76
77
  | `invalid_grant` during refresh | The refresh token expired, was revoked, was replayed, or the grant is no longer usable | Stop retrying, discard the token set, and ask the user to authorize again. If the error recurs for the same user shortly after a successful refresh, investigate concurrent refreshes before reauthorizing. |
78
+ | `invalid_scope` | `scope` is malformed or a refresh request asks for a scope that the authorization did not grant | Omit `scope` to retain the existing scopes, or send only scopes already granted to the refresh token. Refresh cannot expand an authorization's scopes. |
79
+ | `unsupported_grant_type` | `grant_type` names a grant that the token endpoint does not support | Use `authorization_code` when exchanging a code or `refresh_token` when refreshing. Send the exact value in a form-encoded request. |
80
+ | `invalid_target` | A `resource` value is missing where required, malformed, repeated, unknown, or was not granted during authorization | Use the exact resource from authorization-server metadata or the resource previously authorized. After correcting a refresh request, you may retry with the same refresh token because Lunch Money validates the resource before consuming it. |
81
+
82
+ A network failure or `5xx` response is not an OAuth protocol result. Use bounded retries for transient failures only when repeating the request is safe. A refresh may have succeeded even when its response was lost, so never retry a consumed refresh token after receiving `invalid_grant`.
77
83
 
78
84
  An `invalid_grant` response does not distinguish a replayed refresh token from one that expired or is otherwise unknown. Recurrence shortly after a successful refresh is the useful signal: Lunch Money rotates refresh tokens on every use, and replaying a consumed token revokes the entire grant. Serialize refreshes for each grant and persist every replacement before discarding the previous token, as described in [Refresh access](/oauth/tokens#refresh-access).
79
85
 
@@ -84,12 +90,12 @@ If a successful authorization-code exchange does not include a refresh token, co
84
90
  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
91
 
86
92
  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 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.
93
+ 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 budget-account connections unchanged.
88
94
  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
95
 
90
96
  If no eligible refresh token is available, discard the rejected access token and ask the user to authorize again.
91
97
 
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:
98
+ 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 budget account. Every OAuth access-token failure returns the same generic `401` response:
93
99
 
94
100
  ```http
95
101
  HTTP/1.1 401
@@ -129,23 +135,17 @@ WWW-Authenticate: Bearer error="insufficient_scope", scope="transactions:read"
129
135
  }
130
136
  ```
131
137
 
132
- The `scope` parameter names every scope the endpoint requires, including any the client may already have. It is not a list of only the missing scopes. Check the operation in the [V2 API reference](/v2/docs) or the [scope catalog](/oauth/scopes) to confirm its requirements.
138
+ The `scope` parameter names every scope the endpoint requires, including any the client may already have. It is not a list of only the missing scopes. Check the operation in the [V2 API reference](/v2/docs) or [OAuth scopes](/oauth/scopes) to confirm its requirements.
133
139
 
134
140
  ### Replace a client to change scopes
135
141
 
136
142
  Adding `scope` to an authorization URL cannot upgrade or narrow a grant. Lunch Money ignores the requested value and uses the client's complete registered scope set. To change scopes, create a replacement client, test all functionality, complete any required review, update the deployed client ID and credentials, and send every existing user through authorization again.
137
143
 
138
- ## Client-management failures
139
-
140
- The Developer Portal can return structured errors when you register clients, create client secrets, or submit a client for review. These errors use the Lunch Money response format, not OAuth protocol errors such as `invalid_grant`.
144
+ ## Support email is not confirmed
141
145
 
142
- | Status and code | Meaning | What to do |
143
- | --- | --- | --- |
144
- | `422 quota_exceeded`, field `clients` | You already own 25 non-deleted OAuth clients. | Delete a client you no longer use before registering another. |
145
- | `422 quota_exceeded`, field `secrets` | The client already has three non-revoked secrets. Expired secrets still count. | Revoke an obsolete or expired secret before creating another. |
146
- | `429 review_quota_exceeded` | You have made five initial review submissions across your clients in the rolling 24-hour window. | Wait until the time shown in the Developer Portal before submitting again. API clients should also respect the `Retry-After` response header. |
146
+ You cannot submit a client for review until its support email is confirmed. Open the client in the [Developer Portal](/oauth/applications), check that the address is correct, and resend the confirmation email if needed. Check the mailbox's spam or junk folder for a message from `support@lunchmoney.app`.
147
147
 
148
- Cancelled submissions, rejected-client resubmissions, and submissions for clients later deleted still count toward the review limit. Reviews opened automatically after an active client's identity changes do not count.
148
+ Each confirmation link expires after 72 hours. You can resend at most once per hour; the Developer Portal shows when another resend is available. If the link reports that it is invalid or expired, return to the client and request a new one. Changing the support address also invalidates any link sent to the previous address.
149
149
 
150
150
  ## Local callback failures
151
151
 
@@ -12,6 +12,12 @@ If you exceed this limit, you will receive a `429 Too Many Requests` response.
12
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
+ ## OAuth token requests
16
+
17
+ Requests to `/oauth/token`, including refresh requests, use a separate per-IP OAuth limit shared with `/oauth/introspect`. The default is 300 requests per window, and the default window is one minute. These requests do not count against the API limit applied to `/v1/` and `/v2/` resource requests.
18
+
19
+ Because this limit is keyed by IP address, unrelated users behind the same office, mobile-carrier, or other network address translation (NAT) gateway can share it. If the token endpoint returns `429 Too Many Requests`, respect `Retry-After` and back off with jitter instead of repeatedly sending the token request.
20
+
15
21
  ## Rate limit response
16
22
 
17
23
  When you exceed a rate limit, the API returns a `429 Too Many Requests` response:
@@ -75,7 +75,7 @@ const token = process.env.LUNCH_MONEY_ACCESS_TOKEN;
75
75
 
76
76
  ## Best practices
77
77
 
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.
78
+ 1. **Start with a test budget account** — API changes are permanent. Use a test budget account while you're learning so your real data stays safe. See the [Getting Started guide](/getting-started) for instructions.
79
79
 
80
80
  2. **Prefer read-only workflows** — A workflow that only reads data is lower risk than one that can create, update, or delete records.
81
81
 
@@ -95,7 +95,7 @@ If you want to experiment, here are a few low-risk starting points:
95
95
  4. Ask for patterns in merchant names that might deserve new rules or categories.
96
96
  5. Build a reusable "money questions" notebook with prompts you revisit each month.
97
97
 
98
- Start small, and use a test budget while you're learning.
98
+ Start small, and use a test budget account while you're learning.
99
99
 
100
100
  ## Need help?
101
101
 
package/manifest.json CHANGED
@@ -319,7 +319,7 @@
319
319
  { "label": "Troubleshooting", "path": "/oauth/troubleshooting" },
320
320
  { "label": "Review and Approval", "path": "/oauth/review-and-approval" },
321
321
  { "label": "Native Apps", "path": "/oauth/native-apps" },
322
- { "label": "Scope Catalog", "path": "/oauth/scopes" }
322
+ { "label": "Scopes", "path": "/oauth/scopes" }
323
323
  ]},
324
324
  { "section": "REFERENCE", "items": [
325
325
  { "label": "v2 API Overview", "path": "/v2/overview" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunch-money/developer-docs",
3
- "version": "2.11.2-preview.9",
3
+ "version": "2.11.3-preview.1",
4
4
  "description": "Developer documentation content for Lunch Money APIs",
5
5
  "exports": {
6
6
  ".": "./package.json",