OAUTH PREVIEW — Create and manage OAuth clients in the Developer PortalStable portal →

Implement the authorization-code flow

After you register an OAuth 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, 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 budget account associated with the resulting connection.

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 instead. Expo developers can also follow the native Expo sample.

Choose an OAuth library

OAuth libraries handle much of the protocol work for you, including generating PKCE values, building authorization requests, validating callback state, and exchanging authorization codes. Choose a well-maintained OAuth 2.0 client library for your language and application framework. Look for support for:

For Node.js and TypeScript, 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.

The confidential Node.js sample application shows this library in a complete Lunch Money flow. Begin with its src/oauth guide, then follow the listed reading order from protocol types and discovery through authorization, callback validation, credential storage, API access, optional refresh, and revocation.

Configure Lunch Money with discovery

Your OAuth library needs to know where to send the user for authorization, where to exchange codes for tokens, and which protocol features Lunch Money supports. Instead of configuring each value separately, give the library the Lunch Money issuer:

https://api.lunchmoney.dev

Then have it load the OAuth authorization-server metadata document:

https://api.lunchmoney.dev/.well-known/oauth-authorization-server

The metadata document is a machine-readable description of Lunch Money's authorization, token, and revocation endpoints, supported client-authentication methods, and PKCE method. Using it allows your library to obtain the current OAuth configuration without your application assembling endpoint URLs itself.

The resolved endpoint paths are /oauth/authorize, /oauth/token, and /oauth/revoke. These are useful when debugging, but use metadata discovery when your library supports it.

1. Start authorization

On your server, generate:

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.

Lunch Money handles sign-in, budget-account selection, and consent. It may reuse an existing browser login session, and the person may be able to continue with a different Lunch Money user from the authorization screen. A user may have multiple budget accounts, but this authorization is for the one they select. Keep the authorization attempt bound to the authenticated application user who started it; do not assume which Lunch Money user will complete it.

Scopes come from the client registration
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.

2. Handle the callback

The callback receives either code and state, or an OAuth error and the original state. Before exchanging a code:

  1. compare state with the one-time value stored in the initiating session;
  2. reject missing, mismatched, or reused state;
  3. reject unexpected issuer or callback parameters;
  4. consume the stored state and PKCE verifier once; and
  5. remove authorization parameters from the visible browser URL before rendering a page.

Treat access_denied as a normal user decision. Do not log the callback query string.

3. Exchange the code

Send a form-encoded request to the discovered token endpoint with grant_type=authorization_code, the code, exact redirect_uri, and original code_verifier. A confidential client authenticates with HTTP Basic using its client ID and secret. Do this server-side.

Keep the returned credentials in temporary server-side state while you identify the authorization. Do not make an unidentified credential set available to the application or commit it to the normal connection store yet. If a refresh_token is returned, treat it as a high-value credential even during this temporary step.

4. Identify and store the connection

Send the access token in the header:

GET /v2/me HTTP/1.1
Host: api.lunchmoney.dev
Authorization: Bearer YOUR_ACCESS_TOKEN

The operation must be allowed by the token's scope and by the user's access to the selected budget account. See the scope catalog.

Call GET /v2/me immediately after the token exchange, before normal credential persistence. It requires me:read. Strictly validate the response: id identifies the Lunch Money user, account_id identifies the selected budget account, and budget_name is its display name. When reconnecting, use this response to establish the account identity rather than assuming the user selected the same budget. See users, budget accounts, and access tokens.

Store the credential set under the authenticated application user's ID and the returned Lunch Money user id and account_id. Those three fields establish the owner and provider identity for the connection, even if your database also assigns an internal connection ID for routing. Atomically create a complete record when that combination is new, or replace the complete credential set when the same application user reauthorizes the same Lunch Money user and budget-account pair. Persist the access token, refresh token when present, scope, expiration metadata, Lunch Money user id, account_id, and budget_name together, then make that Lunch Money identity and connection active. Do not use budget_name as a key or try to derive identity by parsing the opaque access token.

If GET /v2/me fails validation or the complete record cannot be persisted, discard the temporary credentials and leave existing connections unchanged. A successful token exchange alone must not create an unidentified or partially saved connection.

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.

For later requests, look up the selected connection using both its internal connection ID and the authenticated application user's internal ID. Then verify that its stored Lunch Money user id and account_id belong to the active identity before loading or decrypting credentials. Values supplied by the browser select only a candidate record; they do not establish ownership or provider identity.

When reauthorization completes, replace credentials only in the record matching the authenticated application user and the returned Lunch Money id/account_id pair. A different Lunch Money user authorizing the same shared account_id must create or update a separate owned connection. After an identity change, hide the previous Lunch Money user's connections and clear or partition displayed profile, configuration, and cached financial data. See Associate credentials with your application user for the storage and ownership rules.

5. Continue or recover

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 before implementing refresh, logout, or revocation.

Next: Develop and test your OAuth application.