Transactions API
Transactions belong to a bank account. They come from one of four places: Plaid syncing them from your bank, a Lunch
Flow instance, a file you uploaded, or you typing them in by hand on a manual link. Once a transaction exists, the
interesting thing you do with it is point it at a spending object, which is how monetr knows the money was already
budgeted for.
A few endpoints here only work on manual links. Creating, editing the amount of, and
deleting a transaction all assume monetr owns the record. On a Plaid link the bank owns it, and monetr won't let you
rewrite history it didn't write.
The transaction object
GET List transactions
Returns a bank account's transactions, newest first. This is the feed on the app's main transactions page, and it's the
endpoint you'll reach for most.
In the app: The main transactions feed. It loads 25 at a time and asks for more as you scroll.
GET /api/bank_accounts/:bankAccountId/transactions
Auth: API key, subscription required.
Path parameters
Query parameters
Example
curl --request GET \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/transactions?limit=2" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
[
{
"transactionId": "txn_01j68vszqeq30t7jz7atk9yd9r",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"plaidTransaction": null,
"pendingPlaidTransaction": null,
"amount": 685,
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"spendingAmount": 685,
"createdBySpendingId": null,
"createdByFundingScheduleId": null,
"categories": ["Food and Drink", "Coffee Shop"],
"category": "FOOD_AND_DRINK_COFFEE",
"date": "2024-08-27T05:00:00Z",
"name": "Ruby Coffee Roasters",
"originalName": "SQ *RUBY COFFEE ROASTERS",
"merchantName": "Ruby Coffee Roasters",
"originalMerchantName": "Ruby Coffee Roasters",
"isPending": false,
"uploadIdentifier": null,
"source": "plaid",
"createdAt": "2024-08-27T02:49:28.059Z",
"deletedAt": null
},
{
"transactionId": "txn_01j5m4enxf50k3d2h2c9dtst4z",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"plaidTransaction": null,
"pendingPlaidTransaction": null,
"amount": -204300,
"spendingId": null,
"createdBySpendingId": null,
"createdByFundingScheduleId": null,
"categories": ["Transfer", "Payroll"],
"category": "INCOME_WAGES",
"date": "2024-08-26T05:00:00Z",
"name": "Acme Payroll",
"originalName": "ACME INC DIRECT DEP",
"merchantName": "Acme",
"originalMerchantName": "Acme",
"isPending": false,
"uploadIdentifier": null,
"source": "plaid",
"createdAt": "2024-08-26T11:02:14.221Z",
"deletedAt": null
}
]
Look at the second one. -204300 is a paycheck arriving, not a $2,043 refund. Money coming in is negative. Also notice
spendingAmount is missing there, because that transaction isn't assigned to a spending object.
Errors
GET Get a transaction
Returns one transaction. Handy when you have an ID from somewhere else and want the current state of it.
In the app: The transaction details page, and the upload flow while it waits on a file to finish processing.
GET /api/bank_accounts/:bankAccountId/transactions/:transactionId
Auth: API key, subscription required.
Path parameters
Example
curl --request GET \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/transactions/txn_01j68vszqeq30t7jz7atk9yd9r" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
{
"transactionId": "txn_01j68vszqeq30t7jz7atk9yd9r",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"plaidTransaction": {
"categories": ["Food and Drink", "Coffee Shop"],
"category": "FOOD_AND_DRINK_COFFEE",
"date": "2024-08-27T05:00:00Z",
"authorizedDate": "2024-08-26T05:00:00Z",
"name": "SQ *RUBY COFFEE ROASTERS",
"merchantName": "Ruby Coffee Roasters",
"amount": 685,
"currency": "USD",
"isPending": false,
"createdAt": "2024-08-27T02:49:28.059Z",
"deletedAt": null
},
"pendingPlaidTransaction": null,
"amount": 685,
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"spendingAmount": 685,
"createdBySpendingId": null,
"createdByFundingScheduleId": null,
"categories": ["Food and Drink", "Coffee Shop"],
"category": "FOOD_AND_DRINK_COFFEE",
"date": "2024-08-27T05:00:00Z",
"name": "Ruby Coffee Roasters",
"originalName": "SQ *RUBY COFFEE ROASTERS",
"merchantName": "Ruby Coffee Roasters",
"originalMerchantName": "Ruby Coffee Roasters",
"isPending": false,
"uploadIdentifier": null,
"source": "plaid",
"createdAt": "2024-08-27T02:49:28.059Z",
"deletedAt": null
}
This is the one endpoint that fills in plaidTransaction, holding the record exactly as Plaid sent it. That's where you
look when monetr's cleaned up name and the bank's version disagree and you want to know which is which. The list
endpoint doesn't load the relation, so it returns null there for the very same transaction. Manual, uploaded and Lunch
Flow transactions have nothing to fill in and return null on both.
Errors
GET Find similar transactions
Returns the cluster of transactions that look like the one you asked about, which is how monetr spots recurring
spending. Clusters are built by a background job, so a brand new transaction won't be in one yet.
In the app: The similar transactions panel on the transaction details page.
GET /api/bank_accounts/:bankAccountId/transactions/:transactionId/similar
Auth: API key, subscription required.
Path parameters
Example
curl --request GET \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/transactions/txn_01j68vszqeq30t7jz7atk9yd9r/similar" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
{
"transactionClusterId": "tcl_01j6a2mkxw5rt8p9e3vqn7hd4c",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"signature": "ruby coffee roasters",
"centroid": "txn_01j68vszqeq30t7jz7atk9yd9r",
"name": "Ruby Coffee Roasters",
"originalName": "SQ *RUBY COFFEE ROASTERS",
"members": [
"txn_01j68vszqeq30t7jz7atk9yd9r",
"txn_01j61k7dp3wc8xm2r9tzs4bnhy",
"txn_01j5tq9wr6vj0k4h8bxdm2ncfe"
],
"debug": [
{
"word": "ruby",
"sanitized": "ruby",
"order": 0,
"value": 0.48,
"rank": 1,
"count": 3
}
],
"merchant": [],
"createdAt": "2024-08-27T03:15:02.881Z",
"updatedAt": "2024-08-27T03:15:02.881Z"
}
members includes the transaction you asked about. centroid is whichever member the clustering picked as most
representative, and it can be null. debug and merchant are the term weights the clustering worked from, which are
there for diagnosing bad groupings and aren't worth reading otherwise.
The TransactionCluster model has a rules field, but nothing loads that relation, so it's always omitted from this
response.
Errors
POST Create a transaction
Adds a transaction by hand. Only works on manual links, since on a Plaid link the bank decides what exists.
In the app: Adding a transaction by hand on a manual account.
POST /api/bank_accounts/:bankAccountId/transactions
Auth: API key, subscription required.
Path parameters
The examples below use bac_01j6b4nqx8ws5rt2m9pked7hvc, the manual account created on the bank accounts
page, rather than the Plaid checking account the rest of this page reads from.
The Plaid account would reject both of these.
Body
Send anything else and the whole request is rejected with a 400 and {"problems": {"source": "key not expected"}}. That
catches people out, because the natural move is to fetch a transaction, change one field and post the whole object back.
That won't work here or on any other endpoint in this API. categories, category and source are set by monetr, and
a transaction created this way is always source: "manual".
When adjustsBalance is true, monetr subtracts the amount from the account's available balance, and from the current
balance too if the transaction isn't pending. That's a real edit to the balance monetr reports for the account. Leave it
false and you record the transaction without touching the balance, which is what you want if the balance is already
right.
Example
curl --request POST \
--url "https://my.monetr.local/api/bank_accounts/bac_01j6b4nqx8ws5rt2m9pked7hvc/transactions" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET" \
--header "Content-Type: application/json" \
--data '{
"name": "Reykjavik flights",
"merchantName": "Icelandair",
"amount": 84200,
"date": "2024-08-28T05:00:00Z",
"isPending": false,
"adjustsBalance": true
}'
{
"transaction": {
"transactionId": "txn_01j6d5htqw3zr8mv2npxek4bcf",
"bankAccountId": "bac_01j6b4nqx8ws5rt2m9pked7hvc",
"plaidTransaction": null,
"pendingPlaidTransaction": null,
"amount": 84200,
"spendingId": null,
"createdBySpendingId": null,
"createdByFundingScheduleId": null,
"categories": null,
"category": null,
"date": "2024-08-28T05:00:00Z",
"name": "Reykjavik flights",
"originalName": "Reykjavik flights",
"merchantName": "Icelandair",
"originalMerchantName": "",
"isPending": false,
"uploadIdentifier": null,
"source": "manual",
"createdAt": "2024-08-28T16:41:09.882Z",
"deletedAt": null
},
"balance": {
"bankAccountId": "bac_01j6b4nqx8ws5rt2m9pked7hvc",
"currency": "USD",
"current": 225800,
"available": 225800,
"limit": 0,
"free": 225800,
"expenses": 0,
"goals": 0
}
}
The response carries more than the transaction. balance is the account's recalculated balances, included so a client
doesn't have to make a follow up call. Because adjustsBalance was true, the account dropped from 310000 to 225800.
There's no spending key here because this transaction wasn't assigned to an envelope. Set spendingId to a spending
object on the same bank account and you get a third key, spending, holding that envelope with its balance already
updated.
Note originalName came back matching name, and originalMerchantName is empty. On a manual transaction there's no
bank version to preserve, so monetr copies the name across and leaves the merchant original blank.
Errors
PATCH Update a transaction
Changes a transaction. What you're allowed to change depends on whether the link is manual. On a Plaid link you get the
name, the merchant name, and what it's spent from. On a manual link you also get the amount, the date, and the pending
flag.
In the app: Renaming a transaction on its details page, and changing which envelope it comes out of from the
transaction list.
PATCH /api/bank_accounts/:bankAccountId/transactions/:transactionId
Auth: API key, subscription required.
Path parameters
Body
Send only what you're changing. Everything is optional.
On manual links only, three more:
Sending amount, date or isPending on a Plaid link fails validation. adjustsBalance is rejected here on purpose,
even on manual links: monetr doesn't recalculate balances when an amount changes yet, so rather than accept a flag it
wouldn't honor, it says no.
Example
Move the coffee out of the Coffee envelope and back to free-to-use:
curl --request PATCH \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/transactions/txn_01j68vszqeq30t7jz7atk9yd9r" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET" \
--header "Content-Type: application/json" \
--data '{
"spendingId": null
}'
{
"transaction": {
"transactionId": "txn_01j68vszqeq30t7jz7atk9yd9r",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"plaidTransaction": null,
"pendingPlaidTransaction": null,
"amount": 685,
"spendingId": null,
"createdBySpendingId": null,
"createdByFundingScheduleId": null,
"categories": ["Food and Drink", "Coffee Shop"],
"category": "FOOD_AND_DRINK_COFFEE",
"date": "2024-08-27T05:00:00Z",
"name": "Ruby Coffee Roasters",
"originalName": "SQ *RUBY COFFEE ROASTERS",
"merchantName": "Ruby Coffee Roasters",
"originalMerchantName": "Ruby Coffee Roasters",
"isPending": false,
"uploadIdentifier": null,
"source": "plaid",
"createdAt": "2024-08-27T02:49:28.059Z",
"deletedAt": null
},
"balance": {
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"currency": "USD",
"current": 384219,
"available": 384219,
"limit": 0,
"free": 120359,
"expenses": 218860,
"goals": 45000
},
"spending": [
{
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "expense",
"name": "Coffee",
"targetAmount": 8000,
"currentAmount": 2000,
"usedAmount": 0,
"ruleset": "DTSTART:20240801T050000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1",
"lastSpentFrom": null,
"lastRecurrence": "2024-08-01T05:00:00Z",
"nextRecurrence": "2024-09-01T05:00:00Z",
"nextContributionAmount": 6000,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2022-01-14T18:32:11.044Z"
}
]
}
spending here is an array, not a single object like it is on create. Moving a transaction between envelopes can touch
two of them, so the response carries every spending object that changed. It's an empty array when nothing did.
Errors
DELETE Delete a transaction
Removes a transaction. Manual links only, same reasoning as create.
In the app: Deleting a transaction, behind the confirmation prompt.
DELETE /api/bank_accounts/:bankAccountId/transactions/:transactionId
Auth: API key, subscription required.
Path parameters
Query parameters
Both of these are snake case, unlike everything else in the API.
Example
curl --request DELETE \
--url "https://my.monetr.local/api/bank_accounts/bac_01j6b4nqx8ws5rt2m9pked7hvc/transactions/txn_01j6d5htqw3zr8mv2npxek4bcf?adjusts_balance=true" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
{
"balance": {
"bankAccountId": "bac_01j6b4nqx8ws5rt2m9pked7hvc",
"currency": "USD",
"current": 310000,
"available": 310000,
"limit": 0,
"free": 310000,
"expenses": 0,
"goals": 0
},
"spending": []
}
You don't get the deleted transaction back, only the balances and any spending objects that changed as a result. If the
transaction was assigned to an envelope and you passed adjusts_balance=true, the money goes back into that envelope
and it shows up in spending.
The reverse is the trap: deleting a transaction that's still assigned to an envelope without adjusts_balance=true
leaves that money allocated to the envelope with nothing to show for it. spending comes back empty and the envelope
quietly keeps the deduction. Clear spendingId with a PATCH first if you want the money back.
Errors