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.
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 library to demonstrate:
state and S256 Proof Key for Code Exchange (PKCE);GET /v2/me with a server-held access token;offline_access;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.
openid-client is the recommended and supported third-party Node.js library for working with Lunch Money's OAuth interfaces. The sample uses it for discovery, authorization, callback validation, token exchange, refresh, and revocation while keeping every credential on the server.
You need:
While a client is in development, only the Lunch Money user who created it can authorize it. Use that same Lunch Money user when the sample sends you through authorization. Other Lunch Money users can authorize the client only after it has been reviewed and approved.
Three actions and identities are involved:
local-demo-user. A production application replaces it with an application-specific ID obtained from its authenticated server-side session. The ID should identify the user without using an email address or other personally identifiable information.Your real Lunch Money account
creates and owns the OAuth client
authorizes access to a real budgeting account
↓
Sample OAuth callback
↓
Credentials stored under local-demo-user
(the stand-in for your application's authenticated user)
The sample uses the client you register in the steps below; it does not create or modify that client. Client management remains in the Developer Portal. The automated tests use mocked HTTP responses, but the running sample connects to the configured real Lunch Money authorization and API services.
git clone https://github.com/lunch-money/lm-oauth-confidential-node-example.git
cd lm-oauth-confidential-node-example
npm ci
npm run build
In the Lunch Money Developer Portal, create a Confidential web client with:
http://localhost:4002/oauth/callback;me:read scope;offline_access if you also want to exercise refresh; andLunch Money uses the complete scope set registered for the client. The sample therefore does not send a scope parameter during authorization.
Registered scopes cannot be changed. Decide whether you want to test refresh before creating the client: adding offline_access later requires a replacement client registered with both me:read and offline_access, followed by a new authorization.
You do not need offline_access to authorize the client, call /v2/me, revoke access, or authorize again.
In the same terminal, set the client ID and secret from the Developer Portal:
export OAUTH_CLIENT_ID='YOUR_CLIENT_ID'
export OAUTH_CLIENT_SECRET='YOUR_CLIENT_SECRET'
export OAUTH_REDIRECT_URI='http://localhost:4002/oauth/callback'
export LUNCH_MONEY_API_BASE_URL='https://api-alpha.lunchmoney.app/v2'
The Developer Portal replaces https://api-alpha.lunchmoney.app/v2 with the API base URL for the environment where you registered the client, so the alpha guide configures alpha explicitly. If you are reading this walkthrough in the sample repository with a production client, omit the LUNCH_MONEY_API_BASE_URL export; the sample defaults to https://api.lunchmoney.dev. For a beta or alpha client, use the exact URL supplied by Lunch Money. The client registration and API base URL must belong to the same environment. The sample uses that value for OAuth discovery, token operations, and Lunch Money API requests. A real application should also use OAuth discovery instead of hard-coding authorization, token, and revocation endpoint URLs. PORT is optional and defaults to 4002.
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.
Alternatively, copy env.example to .env as described in Load settings from a .env file.
npm run dev
Open http://localhost:4002 in your browser.
If a form reports Invalid CSRF token, reload the page and try again. This commonly happens when the development server restarts and clears its in-memory browser session while an older page remains open. Simply leaving the unchanged running sample open does not expire the form token.
/v2/mehttp://localhost:4002/oauth/callback, expand How this authorization was processed to follow the callback, server-side code exchange, /v2/me identity check, and resulting active-user decision. Then choose Call /v2/me.The sample calls GET /v2/me from its Node.js server using the server-held access token. It validates the response against the documented userObject schema and displays the returned profile fields. The access token is never sent to the browser.
The processing panel links each teaching step to its implementation. It deliberately does not reveal credential values. To see the normalized credential response while developing locally, set a debugger breakpoint immediately after exchangeCallback() returns in completeAuthorization, then inspect tokenSet. Do not print tokens in browser output or logs.
If you want to verify the access token outside the sample, copy it from the local debugger and read it silently into a temporary shell variable. This keeps the credential itself out of shell history:
read -s LM_ACCESS_TOKEN
curl --fail-with-body \
--header "Authorization: Bearer ${LM_ACCESS_TOKEN}" \
"${LUNCH_MONEY_API_BASE_URL}/v2/me"
unset LM_ACCESS_TOKEN
Press Enter after pasting the token at the hidden prompt. Treat it as a live secret: use a private local terminal, do not paste it directly into a command, and clear the variable when finished. This debugger workflow is for understanding and testing the sample. A production application should inspect encrypted credential records only through privileged operational tooling that redacts secret values.
Open Lunch Money's Connected Apps page in another tab, keeping the sample available so you can return to it. The client you just authorized should appear with the details you registered. If you plan to exercise refresh, return to the sample without revoking access yet; revocation ends this authorization's continuing access.
If you registered the client with offline_access, choose Refresh access token, then choose Call /v2/me again.
Lunch Money replaces both the access token and refresh token after a successful refresh. The sample stores the complete replacement credential set on the server before reporting success. Calling /v2/me again confirms that it is using the replacement access token.
If you registered only me:read, Lunch Money does not issue a refresh token and the refresh button does not appear. Skip this step.
The sample allows an immediate refresh so you can observe the flow. A production application normally decides when to refresh using expiration information from the token response or after an API authentication failure.
Choose Disconnect active budget.
The sample then calls /v2/me with the old access token, requires Lunch Money to return 401, and deletes its locally stored credential. This verification demonstrates that the old access token no longer works.
To repeat the flow, choose Connect Lunch Money and authorize the client again.
To see what happens when a Lunch Money user disconnects the client:
The request should fail because Lunch Money no longer accepts the stored access token. Choose Forget local credential only to remove the now-unusable local credential before starting again.
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.
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.
localhost workslocalhost, so this walkthrough does not require a public deployment or tunnel.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.
Before adapting the code, read the repository's OAuth code guide, security model, production checklist, and troubleshooting guide.
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.
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 budget-account account_id pair.
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.
When refresh succeeds, the sample stores the replacement access token, refresh token, scope, and expiration metadata together without displaying token values. Your refresh-capable application must preserve that boundary, serialize refreshes for each application user and connection, and atomically store the complete rotated credential set. Retry transient failures according to your application's policy, but treat invalid_grant as terminal: stop refreshing, discard unusable credentials, and require reauthorization. Disconnect must invalidate the refresh token as well as the current access token so continuing access cannot resume.
Start with the src/oauth guide. It provides the maintained reading order for the framework-independent OAuth modules and connects each file to the stage of the flow it implements.
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 explains this boundary, and its OAuth tests mirror the recommended reading order, including focused refresh tests. If your result differs, compare the stage that failed with the sample's troubleshooting guide and the public OAuth troubleshooting guide.
Choose lm-oauth-native-expo-example for an iOS or Android application installed
on the user's device when there is no trusted backend that can hold a client
secret. It uses Expo, the platform authentication browser, authorization code
with S256 PKCE, and platform secure storage as a native/public client. Its
optional offline_access path demonstrates rotated refresh credentials and
terminal reauthorization without introducing a backend or client secret.
Do not choose it merely because your interface is responsive. Choose it because the installed application performs OAuth and owns its credential. If a trusted server performs the code exchange and stores credentials, use the confidential Node.js sample above instead.
Unlike the confidential sample, the native sample has no separate application
account or server-side application-user boundary. It identifies local
connections directly by the Lunch Money user id and budget-account
account_id returned by GET /v2/me. It can retain connections for more than
one Lunch Money user on the device, but only the active user's budgets are
visible or selectable. Applications that add their own sign-in system must put
that pair inside the authenticated application user's storage and ownership
boundary.
The native sample defaults to the production API at
https://api.lunchmoney.dev. This Developer Portal is configured for
https://api-alpha.lunchmoney.app/v2. If this differs from production, set the
sample's API-base override before starting it:
export EXPO_PUBLIC_LUNCH_MONEY_API_BASE_URL='https://api-alpha.lunchmoney.app/v2'
Register the OAuth client in the same environment as that API base URL. A client registration from one environment cannot be used with another environment's authorization server and API.
The native repository's walkthrough is the canonical hands-on guide;
complete the registration, development-build, authorization, /v2/me,
optional refresh, revocation, and local-reset walkthrough there.
Start with the native sample's src/oauth guide. 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.
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 to follow the expected success and failure behavior, then review its security guidance and production checklist before adapting it.
Next: Develop and test your OAuth application and operate it securely.