@lunch-money/developer-docs 2.11.0-preview.10

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.
@@ -0,0 +1,1143 @@
1
+ # Migrating from API v1 to v2
2
+
3
+ The v2 API was built with direct input from the Lunch Money developer community. Features and design decisions were shaped by feedback including:
4
+
5
+ - <a href="https://lunchmoney.canny.io/developer-api/p/openapi-spec" target="_blank" rel="noopener noreferrer">OpenAPI Spec</a>
6
+ - <a href="https://lunchmoney.canny.io/developer-api/p/delete-transaction-endpoint" target="_blank" rel="noopener noreferrer">Delete transaction endpoint</a>
7
+ - <a href="https://feedback.lunchmoney.app/developer-api/p/expose-api-for-the-v2-budget-feature" target="_blank" rel="noopener noreferrer">Expose API for the v2 Budget feature</a>
8
+ - <a href="https://lunchmoney.canny.io/developer-api/p/create-tags-endpoint" target="_blank" rel="noopener noreferrer">Create Tags Endpoint</a>
9
+ - <a href="https://lunchmoney.canny.io/developer-api/p/allow-partial-transaction-insertions-or-upserts" target="_blank" rel="noopener noreferrer">Insert Transactions even when there are some duplicates</a>
10
+ - <a href="https://lunchmoney.canny.io/developer-api/p/improve-error-messaging-in-api" target="_blank" rel="noopener noreferrer">Improve error messaging in API</a>
11
+ - <a href="https://lunchmoney.canny.io/developer-api/p/created-at-updated-at" target="_blank" rel="noopener noreferrer">Created At + Updated At for all objects</a>
12
+ - <a href="https://feedback.lunchmoney.app/developer-api/p/special-characters-in-names-arent-handled-well-by-some-apis" target="_blank" rel="noopener noreferrer">Special Characters in names aren't handled well by some APIs</a>
13
+ - <a href="https://feedback.lunchmoney.app/developer-api/p/add-last-updated-after-filter-to-transactions-api" target="_blank" rel="noopener noreferrer">Add filters for created_at and updated_at to GET Transactions API</a>
14
+ - <a href="https://feedback.lunchmoney.app/developer-api/p/account-metadata-hash" target="_blank" rel="noopener noreferrer">Custom Metadata on Manual Accounts</a>
15
+
16
+ This guide covers what changed from v1 to v2 and how to update your integration. For a general introduction to v2 concepts — authentication, response shapes, naming conventions, error handling, and hydration — see the [v2 API Overview](/v2/overview).
17
+
18
+ ## Getting Started
19
+
20
+ Before you begin:
21
+
22
+ 1. Review the [v2 API Overview](/v2/overview) for general concepts
23
+ 2. Test your migration in a development environment
24
+ 3. Keep your v1 integration working as a fallback until migration is complete
25
+
26
+ > [!TIP] Migration Strategy
27
+ > Start with read operations first, then gradually migrate write operations. This allows you to test v2 responses while still relying on v1 for writes during the transition.
28
+
29
+ ## Base URL Changes
30
+
31
+ The base URL for all API requests changes from:
32
+
33
+ ```
34
+ https://api.lunchmoney.dev/v1/
35
+ ```
36
+
37
+ to:
38
+
39
+ ```
40
+ https://api.lunchmoney.dev/v2
41
+ ```
42
+
43
+ ## General Changes
44
+
45
+ ### Error Handling
46
+
47
+ > [!WARNING] Breaking Change
48
+ > The v1 API often returns `200 OK` even when the request fails — the error is in the response body. Update your error handling to check HTTP status codes instead of inspecting 2XX response bodies.
49
+
50
+ The v2 API returns consistent 4XX status codes for errors and validates all requests strictly, including query parameters and request body properties that v1 would silently ignore. See [Error Responses](/v2/overview#error-responses) in the v2 API Overview for the full status code table and error body format.
51
+
52
+ ### Response Codes
53
+
54
+ Two status code changes affect all endpoints:
55
+
56
+ - **POST requests** now return `201 Created` (was `200 OK`)
57
+ - **DELETE requests** now return `204 No Content` with no response body (was `200 OK` with `true`)
58
+
59
+ See [Response Shapes](/v2/overview#response-shapes) in the v2 API Overview for the complete table.
60
+
61
+ ### Updating Objects
62
+
63
+ > [!WARNING] Property Replacement Behavior
64
+ > Complex properties — objects and arrays — are **replaced entirely** by what you send in the PUT body. To add a tag without removing existing ones, fetch the object first, update the array, then PUT the full array back.
65
+
66
+ The v2 PUT pattern (GET → modify → PUT the full object) and all other PUT rules are covered in [Updating Objects](/v2/overview#updating-objects-put) in the v2 API Overview.
67
+
68
+ ### Property Naming Conventions
69
+
70
+ Property names are standardized across all v2 objects. The specific renames are listed in the endpoint sections below. See [Naming Conventions](/v2/overview#naming-conventions) in the v2 API Overview for the general rules.
71
+
72
+ One change worth noting: all `id` fields are now explicitly `integer` type (not `number`). This should not affect most apps.
73
+
74
+ ## Object and Endpoint Specific Changes
75
+
76
+ ### User Object
77
+
78
+ #### Property Renames
79
+
80
+ :::tabs
81
+ @tab v1
82
+ ```javascript
83
+ {
84
+ "user_id": 123,
85
+ "user_name": "John Doe",
86
+ "user_email": "john@example.com"
87
+ }
88
+ ```
89
+
90
+ @tab v2
91
+ ```javascript
92
+ {
93
+ "id": 123,
94
+ "name": "John Doe",
95
+ "email": "john@example.com"
96
+ }
97
+ ```
98
+ :::
99
+
100
+ #### New Properties
101
+
102
+ - `debits_as_negative`: A user preference shown in the Lunch Money app. This no longer changes how transaction amounts are represented in the v2 API, but apps may use this setting to display amounts and balances in the way the user prefers. See the [Amounts & Balances guide](/v2/amounts-and-balances) for details.
103
+
104
+ <a href="./changelog-visual#user-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 differences</a>
105
+
106
+ <a href="/v2/docs#model/userObject">View v2 User Object Docs</a>
107
+
108
+
109
+ **Migration Tips**:
110
+ - Update all references to `user_id`, `user_name`, and `user_email` in your code to use the new property names.
111
+ - Do not use the user's `debits_as_negative` setting to interpret v2 transaction amounts. In the v2 API, positive `amount` values are always debits and negative `amount` values are always credits.
112
+
113
+ ### Categories
114
+
115
+ #### Endpoint Changes
116
+
117
+ #### Creating Categories
118
+
119
+ v1 had separate endpoints for categories and category groups. v2 uses a single endpoint:
120
+
121
+ :::tabs
122
+ @tab v1 - Create category
123
+ ```javascript
124
+ POST /v1/categories
125
+ { "name": "Groceries", ... }
126
+ ```
127
+
128
+ @tab v1 - Create category group
129
+ ```javascript
130
+ POST /v1/categories/group
131
+ { "name": "Food & Drink", "category_ids": [...], ... }
132
+ ```
133
+
134
+ @tab v2 - Single endpoint
135
+ ```javascript
136
+ // Regular category
137
+ POST /v2/categories
138
+ { "name": "Groceries", ... }
139
+
140
+ // Category group
141
+ POST /v2/categories
142
+ { "name": "Food & Drink", "is_group": true, "children": [...] }
143
+ ```
144
+ :::
145
+
146
+ #### Adding to Category Groups
147
+
148
+ v1 had a unique endpoint for adding children to an existing category group.
149
+ v2 allows children to be modified via the `PUT /categories` endpoint.
150
+
151
+ :::tabs
152
+ @tab v1
153
+ ```javascript
154
+ POST /v1/categories/group/:group_id/add
155
+ { "category_ids": [1, 2, 3] }
156
+ ```
157
+
158
+ @tab v2
159
+ ```javascript
160
+ PUT /v2/categories/:id
161
+ { "children": [1, 2, 3] } // Replaces all children
162
+ ```
163
+ :::
164
+
165
+ #### Deleting Categories
166
+ When attempting to delete a category, both the v1 and v2 APIs will return a `dependents` object if other Lunch Money elements (ie: transactions, rules, etc) depend on the category. Both versions also provide a way to force delete the category when this happens but the syntax is slightly different:
167
+
168
+ :::tabs
169
+ @tab v1
170
+ ```javascript
171
+ DELETE /v1/categories/:id/force
172
+ ```
173
+
174
+ @tab v2
175
+ ```javascript
176
+ DELETE /v2/categories/:id?force=true
177
+ ```
178
+ :::
179
+
180
+ #### Response Changes
181
+
182
+ - `POST /categories` now returns the complete category object
183
+ - (v1 returns just the id, ie: `{ "category_id": 123 }`)
184
+ - `PUT /categories/:id` returns the complete updated category object (not just `true`)
185
+ - `DELETE /categories/:id` returns `204 No Content` (not `true`)
186
+
187
+ #### Object Property Changes
188
+
189
+ - `children` property of a category group is never `null` - it's an empty array `[]` for groups with no children
190
+ - There is no `children` property in (non group) categories
191
+ - `group_category_name` has been removed
192
+ - `group_name` is now present on all grouped categories (not just in certain responses)
193
+ - `collapsed` is now present on all category objects. When set to `true` on a category group it will appear collapsed in the Web UI.
194
+
195
+ <a href="./changelog-visual#category-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 differences</a><br>
196
+ <a href="/v2/docs#model/categoryObject">View v2 Category Object Docs</a>
197
+
198
+
199
+
200
+ ##### Query Parameters
201
+
202
+ Categories in the v2 nested response are ordered by the `order` property. When `order` is not set on a category, it falls back to alphabetical ordering.
203
+
204
+ :::tabs
205
+ @tab v1
206
+ ```javascript
207
+ GET /v1/categories?format=nested // or "flattened" - default: flattened
208
+ ```
209
+
210
+ @tab v2
211
+ ```javascript
212
+ GET /v2/categories // Default: nested ordered list
213
+ GET /v2/categories?format=flattened // All categories including duplicates
214
+ GET /v2/categories?is_group=false // Only assignable categories (no groups)
215
+ GET /v2/categories?is_group=true // Only category groups
216
+ ```
217
+ :::
218
+
219
+
220
+
221
+ **Migration**:
222
+ - Add the `format` query parameter to GET /v2/categories requests as needed, the default has changed
223
+ - Update category and category group creation logic to use a single endpoint with `is_group` property
224
+ - Update group management to use `PUT` with `children` array
225
+ - Handle the new query parameters for filtering categories
226
+ - Update code that checks `children === null` to check for empty array instead
227
+ - Update code to use created or modified categories returned directly from `POST` and `PUT` requests.
228
+
229
+ ### Tags
230
+
231
+ #### New Properties
232
+
233
+ The tag object now includes:
234
+ - `created_at`
235
+ - `updated_at`
236
+ - `archived_at`
237
+
238
+ <a href="./changelog-visual#tags-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 differences</a><br>
239
+ <a href="/v2/docs#model/tagObject">View v2 Tag Object Docs</a>
240
+
241
+ #### New Endpoints
242
+
243
+ v2 adds full CRUD operations for tags:
244
+
245
+ ```javascript
246
+ // Get single tag
247
+ GET /v2/tags/:id
248
+
249
+ // Create tag
250
+ POST /v2/tags
251
+ { "name": "Work Expense", ... }
252
+
253
+ // Update tag
254
+ PUT /v2/tags/:id
255
+ { "name": "Business Expense", ... }
256
+
257
+ // Delete tag
258
+ DELETE /v2/tags/:id
259
+ ```
260
+
261
+ #### List Ordering
262
+
263
+ `GET /tags` now returns tags in alphabetical order (matching the GUI).
264
+
265
+ **Migration**: Take advantage of the new tag management endpoints if you need to create or modify tags programmatically.
266
+ ### Transactions
267
+ The transaction object has changed significantly in v2. A transaction has properties that reference other objects in Lunch Money such as categories, accounts, and tags. In v1 certain details for these related objects were "hydrated". In the context of RESTful APIs, "hydration" generally refers to populating an object with all its data. In the Lunch Money APIs, hydration generally relates to providing some details of associated objects that are also referred to by their IDs in the response body.
268
+
269
+
270
+ > [!NOTE] Breaking Change: Dehydrated Responses
271
+ > To optimize the responsiveness of the v2 Lunch Money APIs, the transaction object will no longer be hydrated. Categories, accounts, and tags are now returned as IDs only. Details of these objects can be retrieved by calling the appropriate endpoint using the supplied ID. Developers are encouraged to maintain a local cache of these objects to reduce the number of API calls.
272
+
273
+ :::tabs
274
+ @tab v1 - Hydrated
275
+ ```javascript
276
+ {
277
+ "id": 123,
278
+ "category_id": 456,
279
+ "category_name": "Groceries", // ❌ Not in v2
280
+ "category_group_id": 789, // ❌ Not in v2
281
+ "category_group_name": "Food", // ❌ Not in v2
282
+ "is_income": false, // ❌ Not in v2
283
+ "tags": [ // ❌ Not in v2
284
+ { "id": 1, "name": "Work" }
285
+ ],
286
+ "asset_name": "Checking", // ❌ Not in v2
287
+ "plaid_account_name": "...", // ❌ Not in v2
288
+ ...
289
+ }
290
+ ```
291
+
292
+ @tab v2 - IDs only
293
+ ```javascript
294
+ {
295
+ "id": 123,
296
+ "category_id": 456, // ✅ Still here
297
+ "tag_ids": [1, 2], // ✅ Array of IDs
298
+ "manual_account_id": 789, // ✅ Renamed from asset_id
299
+ "plaid_account_id": 123, // ✅ Still here
300
+ ...
301
+ }
302
+ ```
303
+ :::
304
+
305
+ To get full details, fetch the related objects:
306
+
307
+ ```javascript
308
+ // Get category details
309
+ GET /v2/categories/456
310
+
311
+ // Get account details
312
+ GET /v2/manual_accounts/789
313
+ // OR
314
+ GET /v2/plaid_accounts/123
315
+
316
+ // Get tag details
317
+ GET /v2/tags/1
318
+ ```
319
+
320
+ **Migration Strategy**:
321
+ - Maintain a local cache of categories, accounts, and tags
322
+ - Update your code to use IDs and fetch related objects separately when needed
323
+ - Consider batching requests or using the bulk endpoints to reduce API calls
324
+
325
+ #### Removed Properties
326
+
327
+ The following properties found in the hydrated v1 Transaction Object are no longer present in the v2 Transaction object
328
+
329
+ These category-related properties are no longer included (get from category object):
330
+ - `category_name`
331
+ - `category_group_id`
332
+ - `category_group_name`
333
+ - `is_income`
334
+ - `exclude_from_budget`
335
+ - `exclude_from_totals`
336
+ - `group_category_name`
337
+
338
+ These account-related properties are no longer included (get from account object):
339
+ - `asset_institution_name`
340
+ - `asset_name`
341
+ - `asset_display_name`
342
+ - `asset_status`
343
+ - `plaid_account_name`
344
+ - `plaid_account_mask`
345
+ - `institution_name`
346
+ - `plaid_account_display_name`
347
+
348
+ #### Renamed Properties
349
+ The following properties have been renamed or refactored in the v2 Transaction object
350
+
351
+ - `asset_id` → `manual_account_id`
352
+ - `tags` (array of objects) → `tag_ids` (array of tag id integers)
353
+ - `has_children` → `is_split_parent`
354
+ - `parent_id` → `split_parent_id`
355
+ - `is_group` → `is_group_parent`
356
+ - `group_id` → `group_parent_id`
357
+ - `display_name` → removed (use `payee` directly)
358
+ - `display_notes` → removed (use `notes` directly)
359
+
360
+ These recurring-related properties have changed:
361
+ - `recurring_payee`, `recurring_description`, etc. are now in an `overrides` object
362
+ - Check the transaction's `recurring_id` and fetch from `/v2/recurring/:id` for details
363
+
364
+
365
+ <a href="./changelog-visual#transaction-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 differences</a><br>
366
+ <a href="/v2/docs#model/transactionObject">View v2 Transaction Object Docs</a>
367
+
368
+
369
+ #### Transaction Amounts
370
+ The `debit_as_negative` property has been removed as an optional query parameter and request body property in the `/transactions` endpoints. In the v2 API, positive `amount` values always indicate debit transactions and negative `amount` values always indicate credit transactions, regardless of the user's `debits_as_negative` setting in the Lunch Money app.
371
+
372
+ :::tabs
373
+ @tab v1
374
+ ```javascript
375
+ GET /v1/transactions?debit_as_negative=false // Query parameter
376
+ ```
377
+
378
+ @tab v2
379
+ ```javascript
380
+ GET /v2/transactions
381
+ // Positive amount values indicate debit transactions
382
+ // Negative amount values indicate credit transactions
383
+ ```
384
+ :::
385
+
386
+ > [!WARNING] Behavior Change
387
+ > In the v1 API the optional `debit_as_negative` query parameter could change how transaction amounts were represented. In the v2 API, transaction amount signs are fixed: positive values are debits and negative values are credits. Existing applications that assume positive values are credits need to be updated.
388
+
389
+ See the [Amounts & Balances guide](/v2/amounts-and-balances) for the full sign convention and how `to_base` works for multi-currency accounts.
390
+
391
+
392
+ > [!TIP] Migration Tip
393
+ > Remove `debit_as_negative` query parameters and update your code to interpret positive transaction amounts as debits and negative amounts as credits.
394
+
395
+ #### Status Values
396
+
397
+ :::tabs
398
+ @tab v1
399
+ ```javascript
400
+ "status": "cleared" | "uncleared" | "pending" | "recurring" | "recurring_suggested"
401
+ ```
402
+
403
+ @tab v2
404
+ ```javascript
405
+ "status": "reviewed" | "unreviewed" | "deleted_pending"
406
+ ```
407
+ :::
408
+
409
+ **Migration**: Update status checks:
410
+ - `"cleared"` → `"reviewed"`
411
+ - `"uncleared"` → `"unreviewed"`
412
+ - Transactions with `is_pending: true` always have status `"unreviewed"`
413
+ - Update any code logic that checks if `status` is `pending` to use `is_pending` instead
414
+
415
+ #### Split and Grouped Transactions
416
+
417
+ **Split Transactions**:
418
+ - Parents of split transactions (`is_parent: true`) are not returned by default in `GET /transactions`
419
+ - Split transactions include `split_parent_id` pointing to the parent
420
+ - Use `GET /transactions/:id` to get parent with full `children` array
421
+ - Use `GET /transactions` with query param `include_split_parents=true` to include parents in list
422
+
423
+ **Grouped Transactions**:
424
+ - Transactions that have been grouped are not returned by default
425
+ - Only the group transaction (`is_group: true`) is returned
426
+ - Use `GET /transactions/:id` to get details of grouped transactions in the transaction group's `children` array.
427
+
428
+
429
+ **Migration**: Update code that processes split or grouped transactions to use the details endpoint when info is needed about split parent transactions, or transactions that are now part of a grouped transaction.
430
+
431
+ #### Query Parameters
432
+
433
+ :::tabs
434
+ @tab v1
435
+ ```javascript
436
+ GET /v1/transactions?pending=true&debit_as_negative=false
437
+ ```
438
+
439
+ @tab v2
440
+ ```javascript
441
+ GET /v2/transactions?include_pending=true
442
+ // debit_as_negative removed
443
+ // Positive amount values indicate debit transactions
444
+ // Negative amount values indicate credit transactions
445
+ // New optional parameters:
446
+ // ?include_metadata=true // Include plaid_metadata, custom_metadata
447
+ // ?include_files=true // Include file attachments
448
+ // ?include_children=true // Include children for split/grouped
449
+ // ?include_split_parents=true // Include parent transactions
450
+ // ?created_since=2024-01-01 // Filter by creation timestamp (date or datetime)
451
+ // ?updated_since=2024-01-01T12:00:00Z // Filter by update timestamp (date or datetime)
452
+ ```
453
+ :::
454
+
455
+ **Migration**: Update query parameter names on `GET /transaction`
456
+ - remove `debit_as_negative`
457
+ - add new `include_XXX` params to get additional info in the returned objects
458
+ - use `created_since` and `updated_since` to filter transactions by creation or update timestamps (accepts date YYYY-MM-DD or ISO 8601 datetime format)
459
+
460
+
461
+ #### Creating Transactions
462
+
463
+ Request body changes:
464
+
465
+ :::tabs
466
+ @tab v1
467
+ ```javascript
468
+ POST /v1/transactions
469
+ {
470
+ "transactions": [...],
471
+ "debit_as_negative": false, // ❌ Removed in v2
472
+ ...
473
+ }
474
+ ```
475
+
476
+ @tab v2
477
+ ```javascript
478
+ POST /v2/transactions
479
+ {
480
+ "transactions": [
481
+ {
482
+ "asset_id": 123, // ❌ Renamed
483
+ "manual_account_id": 123, // ✅ New name
484
+ "tags": ["Work"], // ❌ Can't create tags on the fly
485
+ "tag_ids": [1, 2] // ✅ Must use existing tag IDs
486
+ }
487
+ ]
488
+ }
489
+ ```
490
+ :::
491
+
492
+ Response body changes (specific to duplicate detection):
493
+
494
+ :::tabs
495
+ @tab v1 - Success
496
+ ```javascript
497
+ {
498
+ "ids": [new_transaction1.id, ...],
499
+ }
500
+ ```
501
+
502
+ @tab v1 - Duplicate
503
+ ```javascript
504
+ {
505
+ "error": [
506
+ "Key (user_external_id, asset_id, account_id)=(123457891, 178181, 66938) already exists."
507
+ ]
508
+ }
509
+ ```
510
+
511
+ @tab v2 - Success
512
+ ```javascript
513
+ // New transactions ARE inserted, even if some are duplicates
514
+ {
515
+ "transactions": [
516
+ {
517
+ "id": 8
518
+ // rest of full transaction object
519
+ },
520
+ {
521
+ "id": 9,
522
+ // rest of full transaction object
523
+ }
524
+ ],
525
+ "skipped_duplicates": [
526
+ {
527
+ "reason": "duplicate_external_id",
528
+ "request_transactions_index": 1,
529
+ "existing_transaction_id": 2, // id of the existing match
530
+ "request_transaction": { // from the request body
531
+ "date": "2025-06-20",
532
+ "amount": "250.0000",
533
+ "payee": "Fidelity",
534
+ "manual_account_id": 1,
535
+ "external_id": "12345",
536
+ }
537
+ }
538
+ ]
539
+ }
540
+ ```
541
+ :::
542
+
543
+ > [!WARNING] Important Changes
544
+ > - `asset_id` → `manual_account_id`
545
+ > - `tags` array (could include strings to create tags) → `tag_ids` array (only existing IDs)
546
+ > - `plaid_account_id` can now be set when creating transactions (if account allows modifications)
547
+ > - `debit_as_negative` removed - positive amounts are debits and negative amounts are credits
548
+ > - Duplicated transactions (due to `external_id` conflict, or matching `date`, `amount` and account when `skip_duplicates: true` is in the request body), no longer cause the entire request to fail. Non-duplicates are inserted, and duplicates are reported
549
+
550
+ > [!TIP] Migration Steps
551
+ > - Ensure that amounts are properly signed for inserted transactions. Regardless of the value of the users `debits_as_negative` property always use positive numbers for debits and negative numbers for credits.
552
+ > - When creating transactions with tags, create new tags first using `POST /tags` before assigning them
553
+ > - Update changed property names in request bodies
554
+ > - Modify handling of success responses to leverage the full objects for inserted transactions
555
+ > - Check success response for `skipped_duplicates`
556
+
557
+
558
+ #### Updating Transactions
559
+
560
+ :::tabs
561
+ @tab v1
562
+ ```javascript
563
+ PUT /v1/transactions/:id
564
+ {
565
+ "transaction": { ... },
566
+ "split": [...], // ❌ Moved to separate endpoint
567
+ "debit_as_negative": false, // ❌ Removed
568
+ "skip_balance_update": true // ❌ Renamed
569
+ }
570
+ ```
571
+
572
+ @tab v2 - Single transaction
573
+ ```javascript
574
+ PUT /v2/transactions/:id?update_balance=false // Query parameter
575
+ {
576
+ // Just the transaction properties to update OR
577
+ // A full transaction object with at least one updatable property modified
578
+ }
579
+ ```
580
+
581
+ @tab v2 - Bulk update (new)
582
+ ```javascript
583
+ PUT /v2/transactions
584
+ {
585
+ "transactions": [...]
586
+ // Array of full or partial transaction objects to update
587
+ // Each object MUST include the id of the existing transaction to update
588
+ // The rest of the object should be at least one property to update
589
+ }
590
+ ```
591
+ :::
592
+
593
+ **Migration**:
594
+ - Take advantage of new bulk update endpoint where appropriate
595
+ - Migrate code to split transactions to the new endpoint described below
596
+
597
+ #### Splitting and Grouping Transactions
598
+ These endpoints have been modified to follow the standard pattern of using `POST` to create something and `PUT` to modify something, and `DELETE` to remove something.
599
+
600
+ **Splitting Transactions**:
601
+
602
+ :::tabs
603
+ @tab v1
604
+ ```javascript
605
+ PUT /v1/transactions/:id
606
+ { "split": [...] }
607
+ ```
608
+
609
+ @tab v2
610
+ ```javascript
611
+ POST /v2/transactions/split/:id
612
+ { "child_transactions": [
613
+ // objects with at least "amount" and "payee" fields
614
+ ] }
615
+ ```
616
+ :::
617
+
618
+ > [!NOTE] New Capability
619
+ > Child transactions in `POST /transactions/split/{id}` now support `tag_ids`.
620
+
621
+ **Unsplit Transactions**:
622
+
623
+ :::tabs
624
+ @tab v1
625
+ ```javascript
626
+ POST /v1/transactions/unsplit
627
+ { "parent_ids": [...] }
628
+ ```
629
+
630
+ @tab v2
631
+ ```javascript
632
+ DELETE /v2/transactions/split/:parent_id
633
+ ```
634
+ :::
635
+
636
+ <a href="/v2/docs#tag/transactions-split">View v2 Split Transaction Endpoint Docs</a><br>
637
+
638
+ **Grouping Transactions**:
639
+
640
+ :::tabs
641
+ @tab v1
642
+ ```javascript
643
+ POST /v1/transactions/group
644
+ {
645
+ "transactions": [id1, id2, ...],
646
+ // other properties, ie: date, payee
647
+ }
648
+ ```
649
+
650
+ @tab v2
651
+ ```javascript
652
+ POST /v2/transactions/group
653
+ {
654
+ "ids": [id1, id2,...]
655
+ // other properties remain the same
656
+ }
657
+ ```
658
+ :::
659
+
660
+ **Ungroup Transactions**:
661
+
662
+ :::tabs
663
+ @tab v1
664
+ ```javascript
665
+ DELETE /v1/transactions/group/:id
666
+ // Returns 200 with a transactions array of ids that were ungrouped
667
+ ```
668
+
669
+ @tab v2
670
+ ```javascript
671
+ DELETE /v2/transactions/group/:id
672
+ // Returns 204 No Content
673
+ ```
674
+ :::
675
+
676
+ <a href="/v2/docs#tag/transactions-group">View v2 Group Transactions Endpoint Docs</a><br>
677
+
678
+ **Important Changes**:
679
+ - New method and endpoint to split transactions which returns the original transaction with children
680
+ - Mew method and endpoint to unsplit transactions which returns a 204 No Content
681
+ - `ids` property replaces `transactions` on the `POST /transactions/group` endpoint
682
+ - New method and endpoint to ungroup transactions which returns a 204 No Content
683
+
684
+ **Migration**:
685
+ - Move split and group logic to the new endpoints as needed
686
+
687
+ #### Deleting Transactions
688
+ > [!NOTE] New Feature
689
+ > It is now possible to delete transactions via the API!
690
+
691
+ :::tabs
692
+ @tab v1
693
+ ```javascript
694
+ // No delete endpoint available
695
+ ```
696
+
697
+ @tab v2 - Single
698
+ ```javascript
699
+ DELETE /v2/transactions/:id // Single transaction
700
+ ```
701
+
702
+ @tab v2 - Bulk
703
+ ```javascript
704
+ DELETE /v2/transactions // Bulk delete
705
+ { "ids": [1, 2, 3] }
706
+ ```
707
+ :::
708
+
709
+ <a href="/v2/docs#tag/transactions/DELETE/transactions/{id}">View v2 Delete Transactions Endpoint Docs</a><br>
710
+ <a href="/v2/docs#tag/transactions-bulk/DELETE/transactions">View v2 Bulk Delete Transactions Endpoint Docs</a><br>
711
+
712
+
713
+ **Migration**: Update code that may have been working around the lack of delete endpoints.
714
+
715
+ > [!WARNING] Use With Caution!
716
+ > Deletions are permanent. Ensure apps that use these endpoints are very well tested before using on real user data.
717
+
718
+
719
+ #### Transaction Attachments (New)
720
+
721
+ v2 introduces the ability to view, create and delete file attachments to transactions
722
+
723
+ ```javascript
724
+ // Upload attachment
725
+ POST /v2/transactions/:transaction_id/attachments
726
+ // multipart/form-data with file and optional notes
727
+
728
+ // Get attachment details
729
+ GET /v2/transactions/attachments/:file_id
730
+
731
+ // Get download URL
732
+ GET /v2/transactions/attachments/:file_id/url
733
+
734
+ // Delete attachment
735
+ DELETE /v2/transactions/attachments/:file_id
736
+ ```
737
+ <a href="/v2/docs#tag/transactions-files">View v2 Transaction Attachment Endpoint Docs</a><br>
738
+
739
+ ### Recurring Items
740
+ A new `GET v2/recurring_items/{id} endpoint to get a single recurring item has been added.
741
+
742
+ <a href="/v2/docs#tag/recurring_items">View v2 Recurring Endpoint Docs</a><br>
743
+
744
+ #### Object Structure Changes
745
+
746
+ > [!NOTE] Major Reorganization
747
+ > The recurring object's properties are now grouped into three nested objects: `transaction_criteria` (scheduling and amount details), `overrides` (per-occurrence customizations), and `matches` (query results). Previously these were all flat top-level properties.
748
+
749
+ :::tabs
750
+ @tab v1
751
+ ```javascript
752
+ {
753
+ "id": 123,
754
+ "start_date": "2024-01-01",
755
+ "end_date": null,
756
+ "billing_date": "2024-01-15",
757
+ "payee": "Netflix",
758
+ "amount": "15.99",
759
+ "category_id": 456,
760
+ "category_group_id": 789, // ❌ Removed
761
+ "is_income": false, // ❌ Removed (get from category)
762
+ "exclude_from_totals": false, // ❌ Removed (get from category)
763
+ "granularity": "month",
764
+ "quantity": 1,
765
+ "occurrences": { ... },
766
+ "transactions_within_range": [...],
767
+ "missing_dates_within_range": [...],
768
+ "date": "2024-06-04"
769
+ }
770
+ ```
771
+
772
+ @tab v2
773
+ ```javascript
774
+ {
775
+ "id": 123,
776
+ "description": "Netflix subscription",
777
+ "source": "manual",
778
+ "status": "reviewed", // ✅ New: "reviewed" or "suggested"
779
+ "transaction_criteria": {
780
+ "start_date": "2024-01-01", // ✅ Moved here
781
+ "end_date": null, // ✅ Moved here
782
+ "anchor_date": "2024-01-15", // ✅ Renamed from billing_date
783
+ "payee": "Netflix", // ✅ Moved here
784
+ "amount": "15.99", // ✅ Moved here
785
+ "currency": "usd",
786
+ "granularity": "month", // ✅ Moved here
787
+ "quantity": 1, // ✅ Moved here
788
+ "manual_account_id": 123,
789
+ "plaid_account_id": null
790
+ },
791
+ "overrides": {
792
+ "payee": "Netflix Subscription", // ✅ Moved here
793
+ "notes": "Monthly subscription",
794
+ "category_id": 456 // ✅ Moved here
795
+ },
796
+ "matches": {
797
+ "request_start_date": "2024-06-01", // ✅ Renamed from date
798
+ "request_end_date": "2024-06-30", // ✅ New
799
+ "expected_occurrence_dates": [...], // ✅ Renamed from occurrences
800
+ "found_transactions": [ // ✅ Renamed from transactions_within_range
801
+ { "date": "2024-06-15", "transaction_id": 789 }
802
+ ],
803
+ "missing_transaction_dates": [...] // ✅ Renamed from missing_dates_within_range
804
+ }
805
+ }
806
+ ```
807
+ :::
808
+
809
+ <a href="./changelog-visual#recurring-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 Recurring Object differences</a><br>
810
+ <a href="/v2/docs#model/recurringObject">View v2 Recurring Object Docs</a>
811
+
812
+
813
+ #### Query Parameters
814
+
815
+ :::tabs
816
+ @tab v1
817
+ ```javascript
818
+ GET /v1/recurring_items?start_date=2024-06-01
819
+ ```
820
+
821
+ @tab v2
822
+ ```javascript
823
+ GET /v2/recurring?start_date=2024-06-01&end_date=2024-06-30 // Both required if using dates
824
+ GET /v2/recurring?include_suggested=true // Include suggested recurring items
825
+ ```
826
+ :::
827
+
828
+ **Migration**:
829
+ - Update endpoint path
830
+ - Restructure code to access properties in their new locations
831
+ - Update property names (`billing_date` → `transaction_criteria.anchor_date`)
832
+ - Use both `start_date` and `end_date` when specifying a range
833
+
834
+ ### Manual Accounts (formerly Assets)
835
+ The /assets endpoint was renamed to /manual_accounts to align with language used in the web client.
836
+
837
+ #### Endpoint Rename
838
+
839
+ :::tabs
840
+ @tab v1
841
+ ```javascript
842
+ GET /v1/assets
843
+ POST /v1/assets
844
+ PUT /v1/assets/:id
845
+ ```
846
+
847
+ @tab v2
848
+ ```javascript
849
+ GET /v2/manual_accounts
850
+ POST /v2/manual_accounts
851
+ PUT /v2/manual_accounts/:id
852
+ GET /v2/manual_accounts/:id // ✅ New: Get single account
853
+ DELETE /v2/manual_accounts/:id // ✅ New: Delete account
854
+ ```
855
+ :::
856
+
857
+ <a href="/v2/docs#tag/manual_accounts">View v2 Manual Accounts Endpoint Docs</a><br>
858
+
859
+ #### Property Renames
860
+
861
+ ```javascript
862
+ // v1 → v2
863
+ "type_name" → "type"
864
+ "subtype_name" → "subtype"
865
+ "exclude_transactions" → "exclude_from_transactions"
866
+ ```
867
+
868
+ > [!NOTE] Type Name Change
869
+ > Accounts with `type_name: "depository"` now return `type: "cash"`.
870
+
871
+ #### New Properties
872
+
873
+ - `updated_at`
874
+ - `external_id` (API-only, can be set/updated via API)
875
+ - `custom_metadata` (API-only, any valid JSON object < 4MB)
876
+
877
+ <a href="./changelog-visual#manual-account-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 Manual Account Object differences</a><br>
878
+ <a href="/v2/docs#model/manualAccountObject">View v2 Manual Account Object Docs</a>
879
+
880
+ **Migration**:
881
+ - Update all endpoint paths from `/assets` to `/manual_accounts`
882
+ - Update property names in your code
883
+ - Handle the `depository` → `cash` type change
884
+ - Take advantage of new endpoints for single account and delete operations
885
+
886
+ ### Plaid Accounts
887
+
888
+ #### New Endpoints
889
+
890
+ ```javascript
891
+ // v1 - Only list endpoint
892
+ GET /v1/plaid_accounts
893
+
894
+ // v2 - Added single account endpoint
895
+ GET /v2/plaid_accounts
896
+ GET /v2/plaid_accounts/:id
897
+ ```
898
+ <a href="/v2/docs#tag/plaid_accounts">View v2 Plaid Accounts Endpoint Docs</a><br>
899
+
900
+ #### New Response Properties
901
+
902
+ - `allow_transaction_modification`: Boolean indicating if transactions can be modified (enabled by default)
903
+
904
+ <a href="./changelog-visual#plaid-account-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 Plaid Account Object differences</a><br>
905
+ <a href="/v2/docs#model/plaidAccountObject">View v2 Plaid Account Object Docs</a>
906
+
907
+
908
+ #### Fetch Endpoint Changes
909
+
910
+ :::tabs
911
+ @tab v1
912
+ ```ts
913
+ POST /v1/plaid_accounts/fetch
914
+ // Returns: true
915
+ ```
916
+
917
+ @tab v2
918
+ ```ts
919
+ POST /v2/plaid_accounts/fetch
920
+ // Returns: { "plaid_accounts": [...] } // List of accounts that were fetched
921
+ ```
922
+ :::
923
+
924
+ **Migration**: Update fetch response handling to expect an object with `plaid_accounts` array instead of `true`.
925
+
926
+ ### Crypto Balances
927
+
928
+ The v2 API adds crypto endpoints that support the full lifecycle of manual crypto balances. It also adds endpoints for viewing and extending the list of supported manual cryptocurrencies.
929
+
930
+ The v2 API also provides new endpoints for reading and refreshing synced crypto balances. Synced crypto account linking and management are supported in the Lunch Money Web app only.
931
+
932
+ :::tabs
933
+ @tab v1
934
+ ```ts
935
+ GET /v1/crypto //manual balances only
936
+ ```
937
+
938
+ @tab v2
939
+ ```ts
940
+ GET /v2/cryptocurrencies
941
+ POST /v2/cryptocurrencies
942
+ GET /v2/crypto/manual
943
+ GET /v2/crypto/manual/:id
944
+ GET /v2/crypto/synced
945
+ GET /v2/crypto/synced/:id // Returns one synced crypto account with nested balances
946
+ GET /v2/crypto/synced/:id/:symbol
947
+ POST /v2/crypto/synced/:id/refresh
948
+ POST /v2/crypto/manual
949
+ PUT /v2/crypto/manual/:id
950
+ DELETE /v2/crypto/manual/:id
951
+ ```
952
+ :::
953
+
954
+
955
+ <a href="/v2/docs#tag/crypto-manual">View v2 Manual Crypto Docs</a><br>
956
+ <a href="/v2/docs#tag/crypto-synced">View v2 Synced Crypto Docs</a><br>
957
+ <a href="./changelog-visual#crypto-object-row" target="_blank" rel="noopener noreferrer">View v1/v2 Crypto Object differences</a>
958
+
959
+ **Migration**:
960
+ - Replace `GET /v1/crypto` consumers with one of:
961
+ - `GET /v2/crypto/manual` for manual-only balances
962
+ - `GET /v2/crypto/synced` for synced-only balances
963
+ - If your app lets users add manual crypto balances for unsupported symbols, first call `GET /v2/cryptocurrencies` to inspect the supported catalog, then `POST /v2/cryptocurrencies` with a CoinGecko coin-page URL when a symbol is missing
964
+ - Treat `source` as the contract-level differentiator (`manual` vs `synced`)
965
+ - For synced account-specific reads, update handlers to expect a single synced crypto account object with nested `balances` on `GET /v2/crypto/synced/:id`
966
+ - Use `GET /v2/crypto/synced/:id/:symbol` when you need one balance from a synced crypto account
967
+ - For manual deletes, handle `422 Unprocessable Content` when `keep_history` is not explicitly provided and history exists
968
+
969
+ ### Summary Endpoint
970
+ A new `v2/summary` endpoint replaces the `v1/budgets` endpoint.
971
+
972
+ This endpoint significantly refactors the response and aligns with the recently released v2 Budgets feature.
973
+
974
+ > [!NOTE] `include_occurrences=true`
975
+ > When `include_occurrences` is set to `true`, each returned category includes an `occurrences` array.
976
+ > - For aligned ranges, the array includes one occurrence per budget period in range.
977
+ > - For non-aligned ranges, the array includes only budget periods fully contained in range.
978
+ > - You can inspect `start_date` and `end_date` in occurrence objects to choose a fully aligned date range for subsequent requests.
979
+
980
+ <a href="/v2/docs#tag/summary">View v2 Summary Endpoint Docs</a><br>
981
+ <a href="/v2/docs#model/alignedSummaryResponseObject">View v2 Summary Object Docs</a><br>
982
+
983
+ ### budgets endpoints
984
+
985
+ - **`GET /budgets/settings`** has been added <a href="/v2/docs#tag/budgets/GET/budgets/settings">View Docs</a>
986
+ - **`PUT /budgets`** has been added <a href="/v2/docs#tag/budgets/PUT/budgets">View Docs</a>
987
+ - **`DELETE /budgets`** has been added <a href="/v2/docs#tag/budgets/DELETE/budgets">View Docs</a>
988
+
989
+ ### balance_history endpoints (New)
990
+
991
+ - **`GET /balance_history`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
992
+ - **`GET /balance_history/{account_type}/{account_id}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
993
+ - **`GET /balance_history/crypto_synced/{account_id}/{symbol}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
994
+ - **`PUT /balance_history/{account_type}/{account_id}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
995
+ - **`PUT /balance_history/crypto_synced/{account_id}/{symbol}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
996
+ - **`DELETE /balance_history/{account_type}/{account_id}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
997
+ - **`DELETE /balance_history/crypto_synced/{account_id}/{symbol}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
998
+ - **`DELETE /balance_history/entries/{id}`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
999
+ - **`PUT /balance_history/deleted/{account_id}/details`** has been added <a href="/v2/docs#tag/balance_history">View Docs</a>
1000
+
1001
+ ### Common Migration Patterns
1002
+
1003
+ #### Pattern 1: Getting object details for non hydrated responses
1004
+
1005
+ :::tabs
1006
+ @tab v1 - Hydrated
1007
+ ```javascript
1008
+ const transaction = await getTransaction(123);
1009
+ console.log(transaction.category_name); // Direct access
1010
+ ```
1011
+
1012
+ @tab v2 - Fetch separately
1013
+ ```javascript
1014
+ const transaction = await getTransaction(123);
1015
+ const category = await getCategory(transaction.category_id);
1016
+ console.log(category.name); // Access from category object
1017
+ ```
1018
+
1019
+ @tab v2 - With caching
1020
+ ```javascript
1021
+ // Better: Cache categories
1022
+ const categoryCache = new Map();
1023
+ async function getCategoryName(id) {
1024
+ if (!categoryCache.has(id)) {
1025
+ const cat = await getCategory(id);
1026
+ categoryCache.set(id, cat);
1027
+ }
1028
+ return categoryCache.get(id).name;
1029
+ }
1030
+ ```
1031
+ :::
1032
+
1033
+ #### Pattern 2: Handling Response Codes
1034
+
1035
+ :::tabs
1036
+ @tab v1
1037
+ ```javascript
1038
+ // All success responses were 200 OK
1039
+ if (response.status === 200) {
1040
+ const data = response.data; // Might be true, an ID, an error message, or a complex response object
1041
+ }
1042
+ ```
1043
+
1044
+ @tab v2
1045
+ ```javascript
1046
+ // Proper HTTP status codes
1047
+ if (response.status === 201) {
1048
+ // POST - created object in response.body
1049
+ const created = response.body;
1050
+ } else if (response.status === 200) {
1051
+ // GET - requested items are returned
1052
+ // PUT - updated object in response.body
1053
+ const updated = response.body;
1054
+ } else if (response.status === 204) {
1055
+ // DELETE - no response body
1056
+ // Success!
1057
+ }
1058
+ ```
1059
+ :::
1060
+
1061
+ #### Pattern 3: Error Handling
1062
+
1063
+ :::tabs
1064
+ @tab v1
1065
+ ```javascript
1066
+ // Inconsistent error formats
1067
+ try {
1068
+ // Some errors returned as 200 with { error: "message" }
1069
+ // Some errors returned as 200 with { errors: ["message"] }
1070
+ // Some errors returned as 404
1071
+ } catch (e) {
1072
+ // Handle inconsistently
1073
+ }
1074
+ ```
1075
+
1076
+ @tab v2
1077
+ ```javascript
1078
+ // Consistent error format
1079
+ try {
1080
+ const response = await api.post('/transactions', data);
1081
+ } catch (error) {
1082
+ if (error.response.status === 400) {
1083
+ const { message, errors } = error.response.body;
1084
+ errors.forEach(err => {
1085
+ console.error(err.errMsg);
1086
+ });
1087
+ }
1088
+ }
1089
+ ```
1090
+ :::
1091
+
1092
+ #### Pattern 4: Creating New Tags with New Transactions
1093
+
1094
+ :::tabs
1095
+ @tab v1
1096
+ ```javascript
1097
+ // Create tags on the fly
1098
+ POST /v1/transactions
1099
+ {
1100
+ "transactions": [{
1101
+ "tags": ["New Tag"] // Creates tag automatically
1102
+ }]
1103
+ }
1104
+ ```
1105
+
1106
+ @tab v2
1107
+ ```javascript
1108
+ // Step 1: Create tag
1109
+ const tag = await POST /v2/tags { name: "New Tag" };
1110
+
1111
+ // Step 2: Use tag ID
1112
+ POST /v2/transactions
1113
+ {
1114
+ "transactions": [{
1115
+ "tag_ids": [tag.id]
1116
+ }]
1117
+ }
1118
+ ```
1119
+ :::
1120
+
1121
+ ### Testing Your Migration
1122
+
1123
+ 1. **Start with Read Operations**: Update your code to read from v2 endpoints first
1124
+ 2. **Handle Dehydration**: Ensure your code can work with IDs instead of hydrated objects
1125
+ 3. **Update Write Operations**: Migrate create/update/delete operations
1126
+ 4. **Test Edge Cases**:
1127
+ - Split transactions
1128
+ - Grouped transactions
1129
+ - Transactions with recurring items
1130
+ - Category groups with children
1131
+ 5. **Verify Error Handling**: Test various error scenarios to ensure proper handling
1132
+ 6. **Add New Capabilities**: Take advantage of new endpoints to fully manage Transactions, Tags, and Manual Accounts and more.
1133
+
1134
+ ### Need Help?
1135
+
1136
+ If you run into issues during migration:
1137
+
1138
+ - Review the [v2 API documentation](../v2/)
1139
+ - Review the [v2 API Overview](/v2/overview) for general concepts
1140
+ - <a href="https://lunchmoney.app/discord" target="_blank" rel="noopener noreferrer">Join the Lunch Money community on Discord</a> and send a question in the <a href="https://discord.com/channels/842337014556262411/1134594318414389258" target="_blank" rel="noopener noreferrer">Developer API Channel</a>.
1141
+ - [Email our developer advocate](mailto:jp@lunchmoney.app).
1142
+
1143
+ We're here to help make your migration as smooth as possible!