@lunch-money/developer-docs 2.11.1-preview.8 → 2.11.2-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/docs/STYLE_GUIDE.md +84 -0
- package/docs/amounts-and-balances.md +5 -5
- package/docs/branding-your-app.md +2 -2
- package/docs/currencies.md +8 -9
- package/docs/getting-started.md +23 -19
- package/docs/introduction.md +7 -6
- package/docs/locales.md +4 -4
- package/docs/oauth/authorization-code.md +80 -0
- package/docs/oauth/concepts.md +46 -0
- package/docs/oauth/development.md +57 -0
- package/docs/oauth/index.md +28 -0
- package/docs/oauth/native-apps.md +26 -0
- package/docs/oauth/oauth-scope-catalog-design.md +370 -0
- package/docs/oauth/register-client.md +52 -0
- package/docs/oauth/review-and-approval.md +69 -0
- package/docs/oauth/scopes.md +104 -0
- package/docs/oauth/security.md +44 -0
- package/docs/oauth/tokens.md +66 -0
- package/docs/oauth/troubleshooting.md +145 -0
- package/docs/pagination.md +43 -44
- package/docs/rate-limiting.md +45 -45
- package/docs/using-with-ai.md +2 -2
- package/manifest.json +101 -0
- package/package.json +3 -3
- package/v2/docs/intro-to-v2.md +12 -12
- package/v2/docs/migration-guide.md +2 -2
- package/v2/docs/version-history.md +5 -1
- package/v2/images/oauth-authorization-flow.svg +32 -0
- package/v2/images/oauth-token-lifecycle.svg +14 -0
- package/v2/spec/lunch-money-api-v2.yaml +349 -12
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# OAuth security guidance
|
|
2
|
+
|
|
3
|
+
OAuth gives your application permission to work with a user's Lunch Money data. A few deliberate choices about credential storage, callbacks, logging, and recovery help you protect that access and earn users' trust.
|
|
4
|
+
|
|
5
|
+
Before a client can be approved for use by other Lunch Money users, its application must have a publicly accessible privacy policy. The policy gives users a place to understand how the application handles their data before they authorize it. See [application review and approval](/oauth/review-and-approval) for the complete review requirements.
|
|
6
|
+
|
|
7
|
+
## Protect credentials
|
|
8
|
+
|
|
9
|
+
- Keep confidential-client secrets and refresh tokens on the server and in a secret manager or encrypted credential store.
|
|
10
|
+
- Never put a client secret in browser JavaScript, a mobile binary, a URL, source control, analytics, or a support ticket.
|
|
11
|
+
- Avoid logging authorization codes, access tokens, refresh tokens, session cookies, callback query strings, or secrets.
|
|
12
|
+
- Redact credentials from errors and tracing before data leaves the process.
|
|
13
|
+
- Use HTTPS outside the explicitly supported loopback-development case.
|
|
14
|
+
|
|
15
|
+
## Decide where your web application uses access tokens
|
|
16
|
+
|
|
17
|
+
A web application can keep access tokens on its server and have browser code call its own backend. Where direct browser access to the API is supported, it can instead make access tokens available to browser code and call the Lunch Money API from there. OAuth does not require one architecture for every application.
|
|
18
|
+
|
|
19
|
+
Keeping access tokens on the server reduces their exposure to browser code. Using them in the browser can simplify some applications and makes it easier for the signed-in user or support team to inspect Lunch Money API requests and responses in the browser's Network panel.
|
|
20
|
+
|
|
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
|
+
|
|
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.
|
|
24
|
+
|
|
25
|
+
## Secure authorization and callbacks
|
|
26
|
+
|
|
27
|
+
- Generate unpredictable, session-bound, single-use `state` and verify it before exchanging a code.
|
|
28
|
+
- Use PKCE with `S256` for every authorization.
|
|
29
|
+
- Require an exact registered redirect URI and prevent open redirects behind the callback.
|
|
30
|
+
- Exchange each authorization code once and remove callback parameters from the visible URL.
|
|
31
|
+
- Keep authorization results out of browser history where practical and return `Cache-Control: no-store` for sensitive responses.
|
|
32
|
+
- Launch native authorization in the platform browser, never an embedded WebView.
|
|
33
|
+
|
|
34
|
+
## Minimize authority
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
## Operate credentials safely
|
|
39
|
+
|
|
40
|
+
Rotate client secrets on a schedule appropriate for your application's risk and immediately when one may have been exposed. Use overlapping active secrets during a controlled rotation: create the replacement, deploy and verify it, and then revoke the old secret. Coordinate refreshes so two workers do not replay the same rotating token. Make logout revoke server-side credentials and clear local state.
|
|
41
|
+
|
|
42
|
+
If a credential may have been exposed, stop using it, revoke it, rotate the relevant client secret, review logs without copying secret values, and require reauthorization when the grant can no longer be trusted.
|
|
43
|
+
|
|
44
|
+
Next: [Review token recovery](/oauth/tokens) and [troubleshoot failures](/oauth/troubleshooting).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Token lifecycle and recovery
|
|
2
|
+
|
|
3
|
+
Treat expiration, refresh, revocation, and reauthorization as separate events. Your integration should expect every token to stop working eventually.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Teal boxes show the normal token lifecycle. The amber box marks a terminal failure that requires the user to authorize again.
|
|
8
|
+
|
|
9
|
+
## Determine expiration
|
|
10
|
+
|
|
11
|
+
Access tokens are deliberately short-lived. Record when the token response arrives and add its `expires_in` duration to determine the expected expiry. Refresh slightly before that point while accounting for clock skew. Do not parse the opaque access token or assume a fixed lifetime.
|
|
12
|
+
|
|
13
|
+
## Refresh access
|
|
14
|
+
|
|
15
|
+
A refresh token is issued after a successful authorization-code exchange only when the registered client includes `offline_access`. It lasts longer than an access token, but it is not permanent.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
Persist every replacement refresh token before discarding the previous value, and serialize refreshes for a grant. Lunch Money rotates refresh tokens; replay of a consumed token can invalidate the token family. If refresh fails terminally, delete unusable credentials and start a fresh authorization.
|
|
20
|
+
|
|
21
|
+
## Revoke access
|
|
22
|
+
|
|
23
|
+
Send the token to the discovered revocation endpoint using the authentication method appropriate to the client. Revocation returns success without revealing unnecessary token state. Locally discard the access token, refresh token, and related session state even if the network response is ambiguous.
|
|
24
|
+
|
|
25
|
+
> [!TIP] Revocation endpoint
|
|
26
|
+
> 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.
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
:::tabs
|
|
31
|
+
|
|
32
|
+
@tab Confidential web client
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl --request POST 'https://api.lunchmoney.dev/oauth/revoke' \
|
|
36
|
+
--user "$LUNCH_MONEY_CLIENT_ID:$LUNCH_MONEY_CLIENT_SECRET" \
|
|
37
|
+
--header 'Content-Type: application/x-www-form-urlencoded' \
|
|
38
|
+
--data-urlencode "token=$LUNCH_MONEY_ACCESS_TOKEN" \
|
|
39
|
+
--data 'token_type_hint=access_token'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
@tab Native or public client
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl --request POST 'https://api.lunchmoney.dev/oauth/revoke' \
|
|
46
|
+
--header 'Content-Type: application/x-www-form-urlencoded' \
|
|
47
|
+
--data-urlencode "client_id=$LUNCH_MONEY_CLIENT_ID" \
|
|
48
|
+
--data-urlencode "token=$LUNCH_MONEY_ACCESS_TOKEN" \
|
|
49
|
+
--data 'token_type_hint=access_token'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
:::
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
## Recovery rules
|
|
59
|
+
|
|
60
|
+
- A rejected API request may mean the access token expired, was revoked, lacks the required scope, targets the wrong resource, or no longer maps to an accessible account. The `401` response does not identify which cause applies. Follow the [API failure decision tree](/oauth/troubleshooting#api-failures).
|
|
61
|
+
- Retry once only after a coordinated successful refresh; do not loop on authentication errors.
|
|
62
|
+
- Treat a rejected refresh token as a signal to reauthorize, not as a reason to retry indefinitely.
|
|
63
|
+
- Preserve no fixed lifetime assumption in jobs, queues, or database schemas.
|
|
64
|
+
- Ask the user to authorize again after terminal grant or refresh failure.
|
|
65
|
+
|
|
66
|
+
See [troubleshooting](/oauth/troubleshooting) for error-specific guidance.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Troubleshoot OAuth
|
|
2
|
+
|
|
3
|
+
OAuth problems are usually easiest to resolve once you identify where the flow stopped. An OAuth-related failure can occur in one of three places:
|
|
4
|
+
|
|
5
|
+
- **Authorization and callback:** your application sends the user to Lunch Money to authorize access, then Lunch Money returns the browser to your registered callback.
|
|
6
|
+
- **Token endpoint:** your application exchanges an authorization code for tokens or uses a refresh token to request replacements.
|
|
7
|
+
- **v2 API:** your application presents an access token while making a request for Lunch Money data.
|
|
8
|
+
|
|
9
|
+
When collecting diagnostic details, preserve nonsensitive information such as timestamps and error names, but leave credentials and full callback URLs out of logs and support messages.
|
|
10
|
+
|
|
11
|
+
> [!NOTE] Two error formats
|
|
12
|
+
> OAuth protocol endpoints under `/oauth/` use lower-case OAuth error names such as `invalid_grant`. The v2 API uses the Lunch Money response format, with a `message` and an `errors` array whose entries contain `errMsg`.
|
|
13
|
+
|
|
14
|
+
## Authorization endpoint failures
|
|
15
|
+
|
|
16
|
+
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`.
|
|
17
|
+
|
|
18
|
+
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:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
https://example.com/oauth/callback?error=access_denied&state=RETURNED_STATE
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Match on `error` when it is returned, verify `state`, and treat `error_description` as human-readable text that may change.
|
|
25
|
+
|
|
26
|
+
| OAuth error | Likely cause | What to do |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `invalid_request` | A required parameter is missing, duplicated, or malformed | Compare the request with discovery metadata; send `response_type=code`, exact redirect URI, `state`, and PKCE `S256` values. |
|
|
29
|
+
| `invalid_scope` | The client does not have a usable registered scope set | Open the client in the [Developer Portal](/oauth/applications) and review its assigned scopes. Registered clients always use that complete set. |
|
|
30
|
+
| `access_denied` | The user declined or the application is unavailable to that user | Treat denial normally. During development, only the owner may authorize; other users require an active approved application. |
|
|
31
|
+
|
|
32
|
+
### Users cannot authorize an unapproved client
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
### Redirect URI problems
|
|
39
|
+
|
|
40
|
+
If the callback address cannot be matched to one registered for the client, Lunch Money cannot safely return the browser to your application. It stops locally and shows the user a page explaining what happened, rather than redirecting with an `error`. Your application receives no callback for these attempts.
|
|
41
|
+
|
|
42
|
+
The page displays one of two codes, a UTC timestamp, and the application name and public client ID once the client has been resolved.
|
|
43
|
+
|
|
44
|
+
| Code | Likely cause | What to do |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `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). |
|
|
47
|
+
| `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. |
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
## Callback validation failures
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
## Token endpoint failures
|
|
58
|
+
|
|
59
|
+
The token endpoint (`POST /oauth/token`) returns OAuth 2.0 errors rather than the Lunch Money v2 API error format. The error name appears in an `error` field and is always lower case:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"error": "invalid_grant",
|
|
64
|
+
"error_description": "grant request is invalid"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Match on `error`. Treat `error_description` as human-readable text that may change; it does not identify the specific cause.
|
|
69
|
+
|
|
70
|
+
| `error` value | Likely cause | What to do |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `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. |
|
|
73
|
+
| `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. |
|
|
74
|
+
| `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. |
|
|
75
|
+
|
|
76
|
+
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).
|
|
77
|
+
|
|
78
|
+
If a successful authorization-code exchange does not include a refresh token, confirm that the client was registered with `offline_access`. If it was not, create a replacement client that includes `offline_access`; test and review it, then reauthorize users. If it was, contact [developer support](mailto:developer-support@lunchmoney.app).
|
|
79
|
+
|
|
80
|
+
## API failures
|
|
81
|
+
|
|
82
|
+
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:
|
|
83
|
+
|
|
84
|
+
1. On success, save the replacement tokens and retry the API request once. The access token had merely expired.
|
|
85
|
+
2. On `invalid_grant`, treat the authorization as revoked: mark that user's Lunch Money connection as inactive and discard its tokens. Prompt the user to authorize again the next time they sign in rather than repeatedly interrupting them.
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
If no eligible refresh token is available, discard the rejected access token and ask the user to authorize again.
|
|
89
|
+
|
|
90
|
+
An OAuth access token can fail because it expired, access was revoked, the client was disabled, the user reauthorized the application, a token was revoked or rotated, or the user lost access to the budgeting account. Every OAuth access-token failure returns the same generic `401` response:
|
|
91
|
+
|
|
92
|
+
```http
|
|
93
|
+
HTTP/1.1 401
|
|
94
|
+
WWW-Authenticate: Bearer error="invalid_token"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"message": "Unauthorized",
|
|
100
|
+
"errors": [
|
|
101
|
+
{
|
|
102
|
+
"errMsg": "Access token does not exist."
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The response does not identify the cause. Follow the refresh-first recovery path above rather than inferring whether the token expired or another event ended access.
|
|
109
|
+
|
|
110
|
+
### Insufficient scope
|
|
111
|
+
|
|
112
|
+
An `insufficient_scope` response cannot be fixed by retrying, refreshing, or asking the user to authorize the existing client again. It means the client was created without all the scopes required for the operation. This applies to development clients as well as approved clients. The only recovery is to [replace the client](#replace-a-client-to-change-scopes).
|
|
113
|
+
|
|
114
|
+
```http
|
|
115
|
+
HTTP/1.1 403
|
|
116
|
+
WWW-Authenticate: Bearer error="insufficient_scope", scope="transactions:read"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"message": "Forbidden",
|
|
122
|
+
"errors": [
|
|
123
|
+
{
|
|
124
|
+
"errMsg": "Required OAuth scope: transactions:read"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
### Replace a client to change scopes
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
## Local callback failures
|
|
137
|
+
|
|
138
|
+
For local development, use a supported loopback host: `localhost`, `127.0.0.1`, or `[::1]`. Matching depends on the client type:
|
|
139
|
+
|
|
140
|
+
- **Native or public client:** The scheme, host, path, and query string must match exactly. The port is ignored for a loopback redirect, so register one URI without a port, such as `http://localhost/callback`, and let the application use the ephemeral port it binds at runtime.
|
|
141
|
+
- **Confidential web client:** Every component, including the port, must match the registered redirect URI exactly.
|
|
142
|
+
|
|
143
|
+
Host matching is exact for both client types: a redirect registered with `127.0.0.1` does not match one sent with `localhost`. The browser must also be able to reach the application on the selected host and port. See [development and loopback guidance](/oauth/development#local-loopback-callbacks).
|
|
144
|
+
|
|
145
|
+
If the safe guidance here does not resolve the problem, contact [developer support](mailto:developer-support@lunchmoney.app) with the client ID, stage, timestamp, and nonsensitive error code. Do not send tokens, secrets, codes, or cookies.
|
package/docs/pagination.md
CHANGED
|
@@ -6,10 +6,10 @@ The Lunch Money API uses offset-based pagination for the `GET /transactions` end
|
|
|
6
6
|
|
|
7
7
|
When retrieving transactions using `GET /transactions`, the API supports pagination through the `limit` and `offset` query parameters. By default, the endpoint returns up to 1000 transactions per request. If more transactions match your filter criteria, the response includes a `has_more` property set to `true`, indicating that additional pages are available.
|
|
8
8
|
|
|
9
|
-
> [!NOTE] Pagination
|
|
9
|
+
> [!NOTE] Pagination scope
|
|
10
10
|
> Pagination works with all filter parameters including date ranges, account IDs, categories, tags, and status filters. The pagination applies to the filtered result set.
|
|
11
11
|
|
|
12
|
-
## How
|
|
12
|
+
## How pagination works
|
|
13
13
|
|
|
14
14
|
### Parameters
|
|
15
15
|
|
|
@@ -24,13 +24,13 @@ When retrieving transactions using `GET /transactions`, the API supports paginat
|
|
|
24
24
|
- Minimum: `0`
|
|
25
25
|
- Use this to fetch subsequent pages of results
|
|
26
26
|
|
|
27
|
-
### Response
|
|
27
|
+
### Response property
|
|
28
28
|
|
|
29
29
|
- **`has_more`**: A boolean property in the response indicating whether more transactions are available
|
|
30
30
|
- `true`: More transactions match your filter criteria and are available on subsequent pages
|
|
31
31
|
- `false`: All matching transactions have been returned
|
|
32
32
|
|
|
33
|
-
### Basic
|
|
33
|
+
### Basic pagination flow
|
|
34
34
|
|
|
35
35
|
1. Make your first request (optionally with `limit` specified, defaults to 1000)
|
|
36
36
|
2. Check the `has_more` property in the response
|
|
@@ -39,7 +39,7 @@ When retrieving transactions using `GET /transactions`, the API supports paginat
|
|
|
39
39
|
|
|
40
40
|
## Examples
|
|
41
41
|
|
|
42
|
-
### Basic
|
|
42
|
+
### Basic pagination
|
|
43
43
|
|
|
44
44
|
Here's how to fetch transactions page by page:
|
|
45
45
|
|
|
@@ -61,10 +61,10 @@ async function getAllTransactions(accessToken, limit = 1000) {
|
|
|
61
61
|
|
|
62
62
|
const data = await response.json();
|
|
63
63
|
allTransactions.push(...data.transactions);
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
hasMore = data.has_more;
|
|
66
66
|
offset += limit;
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
console.log(`Fetched ${data.transactions.length} transactions (total: ${allTransactions.length})`);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -84,22 +84,22 @@ def get_all_transactions(access_token, limit=1000):
|
|
|
84
84
|
all_transactions = []
|
|
85
85
|
offset = 0
|
|
86
86
|
has_more = True
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
headers = {
|
|
89
89
|
'Authorization': f'Bearer {access_token}'
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
while has_more:
|
|
93
93
|
url = f'https://api.lunchmoney.dev/v2/transactions?limit={limit}&offset={offset}'
|
|
94
94
|
response = requests.get(url, headers=headers)
|
|
95
95
|
data = response.json()
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
all_transactions.extend(data['transactions'])
|
|
98
98
|
has_more = data['has_more']
|
|
99
99
|
offset += limit
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
print(f"Fetched {len(data['transactions'])} transactions (total: {len(all_transactions)})")
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
return all_transactions
|
|
104
104
|
|
|
105
105
|
# Usage
|
|
@@ -119,18 +119,18 @@ ALL_TRANSACTIONS="[]"
|
|
|
119
119
|
|
|
120
120
|
while [ "$HAS_MORE" = "true" ]; do
|
|
121
121
|
echo "Fetching transactions with offset=$OFFSET, limit=$LIMIT..."
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
RESPONSE=$(curl -s -X GET \
|
|
124
124
|
"https://api.lunchmoney.dev/v2/transactions?limit=$LIMIT&offset=$OFFSET" \
|
|
125
125
|
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
TRANSACTIONS=$(echo $RESPONSE | jq -r '.transactions')
|
|
128
128
|
HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
# Merge transactions (simplified - in practice you'd want proper JSON merging)
|
|
131
131
|
COUNT=$(echo $TRANSACTIONS | jq 'length')
|
|
132
132
|
echo "Fetched $COUNT transactions"
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
OFFSET=$((OFFSET + LIMIT))
|
|
135
135
|
done
|
|
136
136
|
|
|
@@ -138,7 +138,7 @@ echo "All transactions fetched"
|
|
|
138
138
|
```
|
|
139
139
|
:::
|
|
140
140
|
|
|
141
|
-
### Pagination with
|
|
141
|
+
### Pagination with date filters
|
|
142
142
|
|
|
143
143
|
Pagination works seamlessly with date range filters:
|
|
144
144
|
|
|
@@ -166,7 +166,7 @@ async function getTransactionsByDateRange(accessToken, startDate, endDate) {
|
|
|
166
166
|
|
|
167
167
|
const data = await response.json();
|
|
168
168
|
allTransactions.push(...data.transactions);
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
hasMore = data.has_more;
|
|
171
171
|
offset += limit;
|
|
172
172
|
}
|
|
@@ -192,9 +192,9 @@ def get_transactions_by_date_range(access_token, start_date, end_date):
|
|
|
192
192
|
offset = 0
|
|
193
193
|
has_more = True
|
|
194
194
|
limit = 1000
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
while has_more:
|
|
199
199
|
params = {
|
|
200
200
|
'start_date': start_date,
|
|
@@ -202,15 +202,15 @@ def get_transactions_by_date_range(access_token, start_date, end_date):
|
|
|
202
202
|
'limit': limit,
|
|
203
203
|
'offset': offset
|
|
204
204
|
}
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
url = f"https://api.lunchmoney.dev/v2/transactions?{urlencode(params)}"
|
|
207
207
|
response = requests.get(url, headers=headers)
|
|
208
208
|
data = response.json()
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
all_transactions.extend(data['transactions'])
|
|
211
211
|
has_more = data['has_more']
|
|
212
212
|
offset += limit
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
return all_transactions
|
|
215
215
|
|
|
216
216
|
# Usage: Get all transactions for January 2024
|
|
@@ -236,17 +236,17 @@ while [ "$HAS_MORE" = "true" ]; do
|
|
|
236
236
|
RESPONSE=$(curl -s -X GET \
|
|
237
237
|
"https://api.lunchmoney.dev/v2/transactions?start_date=$START_DATE&end_date=$END_DATE&limit=$LIMIT&offset=$OFFSET" \
|
|
238
238
|
-H "Authorization: Bearer $ACCESS_TOKEN")
|
|
239
|
-
|
|
239
|
+
|
|
240
240
|
HAS_MORE=$(echo $RESPONSE | jq -r '.has_more')
|
|
241
241
|
COUNT=$(echo $RESPONSE | jq -r '.transactions | length')
|
|
242
242
|
echo "Fetched $COUNT transactions (offset=$OFFSET)"
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
OFFSET=$((OFFSET + LIMIT))
|
|
245
245
|
done
|
|
246
246
|
```
|
|
247
247
|
:::
|
|
248
248
|
|
|
249
|
-
### Handling the `has_more`
|
|
249
|
+
### Handling the `has_more` flag
|
|
250
250
|
|
|
251
251
|
Always check the `has_more` property to determine if you need to fetch more pages:
|
|
252
252
|
|
|
@@ -262,7 +262,7 @@ async function fetchTransactionsPage(accessToken, limit = 1000, offset = 0) {
|
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
const data = await response.json();
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
return {
|
|
267
267
|
transactions: data.transactions,
|
|
268
268
|
hasMore: data.has_more,
|
|
@@ -288,10 +288,10 @@ import requests
|
|
|
288
288
|
def fetch_transactions_page(access_token, limit=1000, offset=0):
|
|
289
289
|
url = f'https://api.lunchmoney.dev/v2/transactions?limit={limit}&offset={offset}'
|
|
290
290
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
291
|
-
|
|
291
|
+
|
|
292
292
|
response = requests.get(url, headers=headers)
|
|
293
293
|
data = response.json()
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
return {
|
|
296
296
|
'transactions': data['transactions'],
|
|
297
297
|
'has_more': data['has_more'],
|
|
@@ -335,9 +335,9 @@ fi
|
|
|
335
335
|
```
|
|
336
336
|
:::
|
|
337
337
|
|
|
338
|
-
## Best
|
|
338
|
+
## Best practices
|
|
339
339
|
|
|
340
|
-
### 1. Use
|
|
340
|
+
### 1. Use appropriate limit values
|
|
341
341
|
|
|
342
342
|
Choose a `limit` value that balances performance and number of requests:
|
|
343
343
|
|
|
@@ -353,7 +353,7 @@ const response = await fetch('/v2/transactions?limit=1000');
|
|
|
353
353
|
const response = await fetch('/v2/transactions?limit=500');
|
|
354
354
|
```
|
|
355
355
|
|
|
356
|
-
### 2. Always
|
|
356
|
+
### 2. Always check `has_more`
|
|
357
357
|
|
|
358
358
|
Don't assume you've received all results. Always check the `has_more` property:
|
|
359
359
|
|
|
@@ -375,7 +375,7 @@ while (hasMore) {
|
|
|
375
375
|
}
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
-
### 3. Combine with
|
|
378
|
+
### 3. Combine with date ranges when possible
|
|
379
379
|
|
|
380
380
|
If you know the date range you need, use `start_date` and `end_date` parameters to reduce the result set size:
|
|
381
381
|
|
|
@@ -387,12 +387,12 @@ const response = await fetch(
|
|
|
387
387
|
|
|
388
388
|
// ❌ Less efficient: Fetches all transactions then filters client-side
|
|
389
389
|
const allTransactions = await fetchAllTransactions();
|
|
390
|
-
const janTransactions = allTransactions.filter(t =>
|
|
390
|
+
const janTransactions = allTransactions.filter(t =>
|
|
391
391
|
t.date >= '2024-01-01' && t.date <= '2024-01-31'
|
|
392
392
|
);
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
-
### 4. Handle
|
|
395
|
+
### 4. Handle edge cases
|
|
396
396
|
|
|
397
397
|
- **Empty results**: The `transactions` array will be empty, and `has_more` will be `false`
|
|
398
398
|
- **Exact page boundary**: If the number of results is exactly divisible by your limit, `has_more` will be `false` on the last page
|
|
@@ -406,24 +406,24 @@ async function safeFetchTransactions(accessToken, limit = 1000, offset = 0) {
|
|
|
406
406
|
headers: { 'Authorization': `Bearer ${accessToken}` }
|
|
407
407
|
}
|
|
408
408
|
);
|
|
409
|
-
|
|
409
|
+
|
|
410
410
|
if (!response.ok) {
|
|
411
411
|
throw new Error(`API error: ${response.status}`);
|
|
412
412
|
}
|
|
413
|
-
|
|
413
|
+
|
|
414
414
|
const data = await response.json();
|
|
415
|
-
|
|
415
|
+
|
|
416
416
|
// Handle empty results
|
|
417
417
|
if (data.transactions.length === 0 && !data.has_more) {
|
|
418
418
|
console.log('No transactions found');
|
|
419
419
|
return [];
|
|
420
420
|
}
|
|
421
|
-
|
|
421
|
+
|
|
422
422
|
return data;
|
|
423
423
|
}
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
-
### 5. Implement
|
|
426
|
+
### 5. Implement rate limit awareness
|
|
427
427
|
|
|
428
428
|
When paginating through many pages, be mindful of rate limits. Consider adding delays between requests:
|
|
429
429
|
|
|
@@ -439,7 +439,7 @@ async function getAllTransactionsWithRateLimit(accessToken) {
|
|
|
439
439
|
allTransactions.push(...data.transactions);
|
|
440
440
|
hasMore = data.has_more;
|
|
441
441
|
offset += limit;
|
|
442
|
-
|
|
442
|
+
|
|
443
443
|
// Small delay to avoid hitting rate limits
|
|
444
444
|
if (hasMore) {
|
|
445
445
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -452,7 +452,7 @@ async function getAllTransactionsWithRateLimit(accessToken) {
|
|
|
452
452
|
|
|
453
453
|
## Troubleshooting
|
|
454
454
|
|
|
455
|
-
### Common
|
|
455
|
+
### Common issues
|
|
456
456
|
|
|
457
457
|
**Issue**: Getting duplicate transactions across pages
|
|
458
458
|
|
|
@@ -506,7 +506,7 @@ const data = await fetch('/v2/transactions?start_date=2024-01-01&end_date=2024-0
|
|
|
506
506
|
// data.transactions.length might be 200, and has_more will be false
|
|
507
507
|
```
|
|
508
508
|
|
|
509
|
-
## Need
|
|
509
|
+
## Need help?
|
|
510
510
|
|
|
511
511
|
If you're experiencing pagination issues that can't be resolved through the techniques described above:
|
|
512
512
|
|
|
@@ -514,4 +514,3 @@ If you're experiencing pagination issues that can't be resolved through the tech
|
|
|
514
514
|
- Verify that you're checking the `has_more` property on each response
|
|
515
515
|
- Consider using date range filters to reduce the result set size
|
|
516
516
|
- [Email our developer advocate](mailto:jp@lunchmoney.app) if you need assistance with pagination implementation
|
|
517
|
-
|