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

AttributeTypeDescription
spendingIdstring (ulid)Identifies the spending object.
bankAccountIdstring (ulid)The bank account this belongs to.
fundingScheduleIdstring (ulid)The funding schedule that fills this.
spendingTypestringexpense or goal. Set at creation and never changes.
namestringWhat you call it. Has to be unique per bank account, per type.
targetAmountintegerHow much needs to be in the envelope by the due date. Has to be greater than zero.
currentAmountintegerHow much is in the envelope right now, in the currency's smallest unit.
usedAmountintegerHow much has been spent out of it. Only meaningful for goals, where it's how you tell a finished goal from an empty one.
rulesetstring, nullableThe recurrence rule, as an RFC 5545 string like DTSTART:20240801T050000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1. Always null on goals.
lastSpentFromtimestamp, nullableIntended to record when a transaction last took money out of this. Nothing in monetr writes it today, so it is always null. Don't build on it.
lastRecurrencetimestamp, nullableThe previous due date. Null until it's come due at least once.
nextRecurrencetimestampThe next due date, as midnight in your account's timezone.
nextContributionAmountintegerHow much the next funding event will put in. monetr recalculates this whenever something relevant changes.
isBehindbooleanWhether monetr thinks it can't get to targetAmount in time given the funding schedule. This is the flag behind the warning in the app.
isPausedbooleanWhether contributions are stopped. A paused object keeps whatever's in it but stops receiving more.
autoCreateTransactionbooleanWhether monetr should write a transaction by itself when this comes due. Expenses on manual links only.
createdAttimestampWhen it was created.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account whose envelopes you want.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account it belongs to.
spendingIdstring (ulid)YesThe spending object you want.

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

StatusWhen
400Either ID is malformed.
404No such spending object on that bank account.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account it belongs to.
spendingIdstring (ulid)YesThe spending object whose transactions you want.

Query parameters

AttributeTypeRequiredDescription
limitintegerNoHow many to return. Defaults to 25, between 1 and 100.
offsetintegerNoHow many to skip. Defaults to 0.

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

StatusWhen
400An ID is malformed, or limit is outside 1 to 100.
404No such spending object on that bank account.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account to create it on.

Body

Shared by both types:

AttributeTypeRequiredDescription
fundingScheduleIdstring (ulid)YesThe funding schedule that'll fill this. Has to already exist on the same bank account.
namestringYesWhat to call it.
nextRecurrencetimestampYesThe due date. This one is fussy, see below.
spendingTypestringYesexpense or goal.
targetAmountintegerYesHow much you need by the due date, in the smallest unit. Must be greater than zero.
currentAmountintegerNoHow much is already in it. Leave the key out and it starts at 0. Sending 0 explicitly is rejected with "Current amount must be greater than zero", so omit it rather than serializing a zero.

Expenses also take:

AttributeTypeRequiredDescription
rulesetstringYesThe recurrence rule as an RFC 5545 string. Required on expenses, rejected on goals. See the cookbook.
autoCreateTransactionbooleanNoHave monetr write the transaction itself when this comes due. Manual links only.

Goals also take:

AttributeTypeRequiredDescription
isPausedbooleanNoStart it paused. Only accepted on goals, expenses can't be paused at creation.

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

StatusWhen
400nextRecurrence is in the past or isn't midnight in your timezone, you sent a ruleset on a goal or left it off an expense, targetAmount is zero or negative, the funding schedule doesn't exist, or you asked for autoCreateTransaction on a goal or a non-manual link.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account it belongs to.
spendingIdstring (ulid)YesThe spending object to change.

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:

AttributeTypeRequiredDescription
fundingScheduleIdstring (ulid)NoMove it to a different funding schedule.
namestringNoRename it.
nextRecurrencetimestampNoChange the due date. Gets snapped to midnight in your timezone.
targetAmountintegerNoChange the target. Must be greater than zero.

Expenses only:

AttributeTypeRequiredDescription
autoCreateTransactionbooleanNoManual links only.
rulesetstringNoA new recurrence rule.

Goals only:

AttributeTypeRequiredDescription
isPausedbooleanNoStop or resume contributions.

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

StatusWhen
400You sent a goal-only field on an expense or the other way around, targetAmount is zero or negative, or autoCreateTransaction on a non-manual link.
404No such spending object.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account whose envelopes you're moving money between.

Body

AttributeTypeRequiredDescription
amountintegerYesHow much to move, in the smallest unit. Has to be greater than zero, so reverse the direction rather than sending a negative.
fromSpendingIdstring (ulid), nullableNoWhere the money comes from. Leave it out to take it from free-to-use.
toSpendingIdstring (ulid), nullableNoWhere it goes. Leave it out to send it back to free-to-use.

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

StatusWhen
400amount is zero or negative, you sent neither fromSpendingId nor toSpendingId, or the source doesn't have enough in it.
404One of the spending objects doesn't exist on that bank account.

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

AttributeTypeRequiredDescription
bankAccountIdstring (ulid)YesThe bank account it belongs to.
spendingIdstring (ulid)YesThe spending object to delete.

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

StatusWhen
400Either ID is malformed.
404No such spending object on that bank account.