Spending API
Spending objects are the envelopes. Each one sits on a bank account, has a
target amount, and gets filled up a little at a time by a funding schedule. When you spend money
on something, you point the transaction at the envelope it came out of, and monetr takes the money from there instead of
from your free-to-use balance.
There are two kinds, and the difference matters for almost every endpoint here.
Expenses repeat. Rent every month, insurance every six months. They have a recurrence rule, and when the due date
comes around the envelope empties and starts filling again for the next one.
Goals don't repeat. You're saving up for one thing by one date, and when you get there you're done. Goals have no
recurrence rule at all, and sending one is an error.
Both are the same object with the same fields, so spendingType is the thing to branch on.
The spending object
GET List spending objects
Returns every expense and goal on a bank account.
In the app: The expenses and goals pages, and the transaction list when it shows which envelope something came out
of.
GET /api/bank_accounts/:bankAccountId/spending
Auth: API key, subscription required.
Path parameters
Example
curl --request GET \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
[
{
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "expense",
"name": "Coffee",
"targetAmount": 8000,
"currentAmount": 1315,
"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": 6685,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2022-01-14T18:32:11.044Z"
},
{
"spendingId": "spnd_01j3d7k2vwqrx8m5t9pnbeh4cz",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "goal",
"name": "New Laptop",
"targetAmount": 250000,
"currentAmount": 45000,
"usedAmount": 0,
"ruleset": null,
"lastSpentFrom": null,
"lastRecurrence": null,
"nextRecurrence": "2025-03-01T06:00:00Z",
"nextContributionAmount": 14642,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2024-07-14T09:03:52.771Z"
}
]
The goal has ruleset and lastRecurrence as null, which is what a goal always looks like.
GET Get a spending object
Returns one expense or goal.
In the app: The expense and goal detail pages.
GET /api/bank_accounts/:bankAccountId/spending/:spendingId
Auth: API key, subscription required.
Path parameters
Example
curl --request GET \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending/spnd_01fpwx67z8djhb8bqcy17mrzfe" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
{
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "expense",
"name": "Coffee",
"targetAmount": 8000,
"currentAmount": 1315,
"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": 6685,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2022-01-14T18:32:11.044Z"
}
Errors
GET List a spending object's transactions
Returns the transactions that were spent out of one envelope, so you can see where the money in it went.
In the app: The list of transactions on an expense's detail page.
GET /api/bank_accounts/:bankAccountId/spending/:spendingId/transactions
Auth: API key, subscription required.
Path parameters
Query parameters
Example
curl --request GET \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending/spnd_01fpwx67z8djhb8bqcy17mrzfe/transactions?limit=1" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
Returns an array of transaction objects, same shape as List transactions.
[
{
"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
}
]
Errors
POST Create a spending object
Makes a new expense or goal. Which fields are valid depends entirely on which one you're making, and monetr validates
the whole body against one shape or the other.
In the app: The new expense and new goal forms.
POST /api/bank_accounts/:bankAccountId/spending
Auth: API key, subscription required.
Path parameters
Body
Shared by both types:
Expenses also take:
Goals also take:
nextRecurrence has two rules that catch people.
It has to be midnight in your account's timezone. If your account is America/Chicago, that means
2024-09-01T05:00:00Z in summer and 2024-12-01T06:00:00Z in winter, because the offset moves with daylight saving.
Don't hardcoded an offset, convert through the timezone. The check is on the hour, so any time in the midnight hour
passes and gets truncated, but send noon and you get a 400.
And it can't be in the past, which is stricter than it sounds: midnight today has already happened, so the earliest
value that works is midnight tomorrow. A date picker defaulting to today will fail here.
Example
curl --request POST \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET" \
--header "Content-Type: application/json" \
--data '{
"spendingType": "expense",
"name": "Coffee",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"targetAmount": 8000,
"ruleset": "DTSTART:20240801T050000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1",
"nextRecurrence": "2024-09-01T05:00:00Z"
}'
{
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "expense",
"name": "Coffee",
"targetAmount": 8000,
"currentAmount": 0,
"usedAmount": 0,
"ruleset": "DTSTART:20240801T050000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1",
"lastSpentFrom": null,
"lastRecurrence": null,
"nextRecurrence": "2024-09-01T05:00:00Z",
"nextContributionAmount": 8000,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2024-08-28T11:07:44.318Z"
}
nextContributionAmount came back calculated. monetr worked out how many funding events happen before the due date and
divided the shortfall across them.
Errors
PATCH Update a spending object
Changes an existing expense or goal. Send only what you're changing.
In the app: Editing an expense or goal from its detail page.
PATCH /api/bank_accounts/:bankAccountId/spending/:spendingId
Auth: API key, subscription required.
Path parameters
Body
Everything is optional, but which keys are allowed depends on the existing object's spendingType. You can't change the
type itself.
On both:
Expenses only:
Goals only:
Changing targetAmount, fundingScheduleId, nextRecurrence or ruleset makes monetr recalculate
nextContributionAmount. So does unpausing. Pausing doesn't, since the number would be stale by the time you unpause
anyway.
currentAmount, usedAmount and isBehind are not editable here. Money moves between envelopes with
transfer, not by writing amounts directly.
Example
curl --request PATCH \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending/spnd_01fpwx67z8djhb8bqcy17mrzfe" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET" \
--header "Content-Type: application/json" \
--data '{
"targetAmount": 12000
}'
{
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "expense",
"name": "Coffee",
"targetAmount": 12000,
"currentAmount": 1315,
"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": 10685,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2022-01-14T18:32:11.044Z"
}
Errors
POST Transfer between spending objects
Moves already-allocated money from one envelope to another, or between an envelope and free-to-use. This is what you use
when you've decided the vacation fund matters more than the new laptop this month.
In the app: The transfer dialog, where you move money between envelopes.
POST /api/bank_accounts/:bankAccountId/spending/transfer
Auth: API key, subscription required.
Path parameters
Body
You have to send at least one of fromSpendingId and toSpendingId. Sending neither is a 400. Sending both moves money
directly between two envelopes without touching free-to-use.
Example
Move $50 out of the laptop goal and into the coffee envelope:
curl --request POST \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending/transfer" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET" \
--header "Content-Type: application/json" \
--data '{
"fromSpendingId": "spnd_01j3d7k2vwqrx8m5t9pnbeh4cz",
"toSpendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"amount": 5000
}'
{
"balance": {
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"currency": "USD",
"current": 384219,
"available": 384219,
"limit": 0,
"free": 121044,
"expenses": 223175,
"goals": 40000
},
"spending": [
{
"spendingId": "spnd_01j3d7k2vwqrx8m5t9pnbeh4cz",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "goal",
"name": "New Laptop",
"targetAmount": 250000,
"currentAmount": 40000,
"usedAmount": 0,
"ruleset": null,
"lastSpentFrom": null,
"lastRecurrence": null,
"nextRecurrence": "2025-03-01T06:00:00Z",
"nextContributionAmount": 15000,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2024-07-14T09:03:52.771Z"
},
{
"spendingId": "spnd_01fpwx67z8djhb8bqcy17mrzfe",
"bankAccountId": "bac_01gds6eqsq7h5mgevwtmw3cyxb",
"fundingScheduleId": "fund_01fpwx4xhvr8gs2fpsafd7nvpz",
"spendingType": "expense",
"name": "Coffee",
"targetAmount": 12000,
"currentAmount": 6315,
"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": 5685,
"isBehind": false,
"isPaused": false,
"autoCreateTransaction": false,
"createdAt": "2022-01-14T18:32:11.044Z"
}
]
}
Both envelopes come back with recalculated contribution amounts, along with the account's new balances. spending has
one entry when you only sent one side, two when you sent both.
Notice the account's current and available didn't change. Transferring doesn't move real money, it changes how
monetr has the existing balance labeled.
Errors
DELETE Delete a spending object
Removes an expense or goal. Whatever was allocated to it goes back to free-to-use, and any transactions that pointed at
it get their spendingId cleared rather than being deleted themselves.
In the app: Deleting an expense or goal from its detail page.
DELETE /api/bank_accounts/:bankAccountId/spending/:spendingId
Auth: API key, subscription required.
Path parameters
Example
curl --request DELETE \
--url "https://my.monetr.local/api/bank_accounts/bac_01gds6eqsq7h5mgevwtmw3cyxb/spending/spnd_01j3d7k2vwqrx8m5t9pnbeh4cz" \
--user "$MONETR_API_KEY_ID:$MONETR_API_KEY_SECRET"
Returns 200 with an empty body. Unlike transfer and the transaction endpoints, you don't get updated balances back, so
re-fetch them if you're tracking that.
Errors