Downloading Balance History from Mint.com

I recently learned that Mint is shutting down, which is a bit of a bummer because I've been using them for 10 years as a way to keep an eye on my finances. The replacement service through Credit Karma looks feature incomplete and it will only preserve three years of history, which is a bit of a non-starter for me. I will most likely be looking for something else, but in the meantime, I've been looking for a way to preserve all my data, so I can take it with me.

The two types of data that are important to me are the transaction history and the balance history. The former is fairly straightforward to do through the web interface (though it has to be done in multiple parts since mint caps exports to 10k transactions), but the balance history is tricker. Under Trends > Net Worth, I can set the filters to a single account and export the history to CSV, but the data will only contain monthly balances. However, I noticed that Mint internally has daily balance history, because when I manually set the date range to any range of 44 days or less, the daily balances show up. This is impractical to do for every account manually, so I went to work writing a script to automate this.

Version 1: Bash Scripts

For my first attempt, I used the developer console to find the rest API calls and copied it as a curl command:

I took this command, piped it into jq to convert the data to CSV, and wrapped it in a bash loop over the data range. The end result looked something like this:

#!/bin/bash -e

ID=...
COOKIE=...
OUTPUT=data.csv
echo "Date,Amount" > "$OUTPUT"

DATE="2007-02-01"  # Adjust based on your data to make it run faster
NOW="$(date +%Y-%m-%d)"
while [[ $DATE < $NOW ]]; do
  curl 'https://mint.intuit.com/pfm/v1/trends' \
    -H 'authority: mint.intuit.com' \
    -H 'accept: application/json, text/plain, */*' \
    -H 'accept-language: en-US,en;q=0.9' \
    -H 'authorization: Intuit_APIKey intuit_apikey=prdakyresYC6zv9z3rARKl4hMGycOWmIb4n8w52r,intuit_apikey_version=1.0' \
    -H 'content-type: application/json' \
    -H "cookie: $COOKIE" \
    -H 'intuit_tid: 27b096e4-5d76-4e55-958a-d01430a39a97' \
    -H 'origin: https://mint.intuit.com' \
    -H 'referer: https://mint.intuit.com/trends' \
    -H 'sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"' \
    -H 'sec-ch-ua-mobile: ?0' \
    -H 'sec-ch-ua-platform: "Windows"' \
    -H 'sec-fetch-dest: empty' \
    -H 'sec-fetch-mode: cors' \
    -H 'sec-fetch-site: same-origin' \
    -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0' \
    --compressed \
    --data @- <<EOF | jq -r '.Trend[]? | [.name, .amount] | @csv' >> "$OUTPUT"
{
  "reportView": {
    "type": "NET_WORTH"
  },
  "searchFilters": [
    {
      "matchAll": true,
      "filters": [
        {
          "type": "AccountIdFilter",
          "accountId": "$ID"
        }
      ]
    },
    {
      "matchAll": false,
      "filters": []
    }
  ],
  "dateFilter": {
    "type": "CUSTOM",
    "startDate": "$DATE",
    "endDate": "$(date +%Y-%m-%d -d "$DATE + 43 days")"
  },
  "offset": 0,
  "limit": 1000
}
EOF

  DATE=$(date +%Y-%m-%d -d "$DATE + 44 days")
done

Bash code to download Mint.com balance history

This generally worked, but it was a bit annoying to need to update my cookie whenever my Mint web session was logged out. It was also a bit slow, which could run into issues if Mint auto logs me out due to inactivity. So, I started looking at some other options.

Version 2: JavaScript

In the developer console, I noticed I could also copy the REST API network request as a JavaScript fetch() statement. I got an idea that perhaps I could write a little script that could be executed in the JavaScript console. The nice thing about that is that I don't need to manually update the cookie, because the browser automatically includes that when the request is sent. I can also do more advanced data processing and send queries in parallel. The end result was:

await (async function(console) {
  let begin = new Date();

  // Fetch list of accounts
  let accounts = await fetch("https://mint.intuit.com/pfm/v1/accounts?offset=0&limit=1000", {
    "headers": {
      "authorization": "Intuit_APIKey intuit_apikey=prdakyresYC6zv9z3rARKl4hMGycOWmIb4n8w52r,intuit_apikey_version=1.0",
      "content-type": "application/json",
      "intuit_tid": "0dc9c908-ef3d-4d97-93eb-a676a74b0cb2",
    },
    "body": null,
    "method": "GET",
    "mode": "cors",
    "credentials": "include"
  }).then(
    response => response.json()
  ).then(
    result => result.Account ?? []
  );
  console.log("Accounts:", accounts);

  // data[date][account] = amount
  let data = {};
  for (const [index, account] of accounts.entries()) {
    const isDebtAccount = (account.type == "CreditAccount" || account.type == "LoanAccount");
    let ids = [account.id];
    if (isDebtAccount) {
      // Querying net worth doesn't seem to work for debt accounts, unless
      // there is at least one assert account that covers the same date range.
      // To work around this, add the bank accounts to the query, so that a
      // mix of ASSET and DEBT entries are returned. (The ASSET entries are
      // filtered out below).
      ids.push(...accounts.filter(({type}) => (type == 'BankAccount')).map(({id}) => id));
    }
    console.log(`Fetching account ${index+1}/${accounts.length}: ${account.name}`)
    let queries = [];
    for (let start = new Date('2007-01-01'); start < new Date(); start.setUTCDate(start.getUTCDate()+44)) {
      // Query data for date range in parallel
      let end = new Date(start-1); end.setUTCDate(end.getUTCDate()+44);
      queries.push(fetch("https://mint.intuit.com/pfm/v1/trends", {
        "headers": {
          "authorization": "Intuit_APIKey intuit_apikey=prdakyresYC6zv9z3rARKl4hMGycOWmIb4n8w52r,intuit_apikey_version=1.0",
          "content-type": "application/json",
          "intuit_tid": "0dc9c908-ef3d-4d97-93eb-a676a74b0cb2",
        },
        "body": JSON.stringify({
          "reportView": {
            "type": "NET_WORTH"
          },
          "searchFilters": [
            {
              "matchAll": true,
              "filters": ids.map(id => ({ "type": "AccountIdFilter", "accountId": id })),
            },
          ],
          "dateFilter": {
            "type": "CUSTOM",
            "startDate": start.toISOString().substring(0,10),
            "endDate": end.toISOString().substring(0,10),
          },
          "offset": 0,
          "limit": 1000,
        }),
        "method": "POST",
        "mode": "cors",
        "credentials": "include",
      }).then(
        response => response.json()
      ).then(result => {
        let trends = result.Trend ?? [];
        //console.log("Trends:", trends)
        for (const {name: date, type, amount} of trends) {
          if (isDebtAccount && type == 'DEBT') {
            (data[date] ??= {})[account.id] = -amount;
          } else if (!isDebtAccount && type == 'ASSET') {
            (data[date] ??= {})[account.id] = amount;
          }
        }
      }));

      // Wait for some requests to complete so that we don't get trottled
      if (queries.length >= 10) {
        await Promise.all(queries);
        queries = [];
      }
    }
    // Wait for remaining requests to complete
    await Promise.all(queries);

    // Wiggle mouse to keep from getting logged out
    document.body.dispatchEvent(new MouseEvent("mousedown"))
  }

  // Build csv content
  console.log("Data:", data);
  let csv_lines = [[`"Date"`, ...accounts.map(({name}) => `"${name}"`)].join(",")];
  for (const date of Object.keys(data).sort((a, b) => (new Date(a) - new Date(b)))) {
    csv_lines.push([`"${date}"`, ...accounts.map(({id}) => `"${data[date][id] ?? ""}"`)].join(","));
  }

  // Save csv file
  const blob = new Blob([csv_lines.join("\r\n")], {type: 'text/csv'});
  let a = document.createElement('a');
  a.download = "mint_bal_history.csv"
  a.href = window.URL.createObjectURL(blob)
  a.dataset.downloadurl =  ['text/csv', a.download, a.href].join(':')
  a.dispatchEvent(new MouseEvent("click"))
  console.log(`Finished in ${(new Date() - begin)/60000} minutes`)
})(console)

JavaScript code to download Mint.com balance History

The script prints progress as it goes. For 10 years of data and 88 accounts, it takes about 12 minutes to run. When it finishes, it downloads a CSV file that looks something like:

Date Membership Share Money Mkt Advantage Free Checking ...
2/19/2013 8007.6 5479.63 ...
2/20/2013 8007.6 5479.63 ...
2/21/2013 8007.6 479.63 ...
... ... ... ... ..
11/6/2016 5216.34 0 3503.31 ..
11/7/2016 5216.34 0 1567.27 ..
... ... ... ... ..

(The blank entries for Membership Share are because that account did not exist until later, so there is no data).

How to Run this for Yourselves

For those of you interested in running this yourselves, I would urge caution regarding running code from strangers on the Internet, especially if you do not know how to read JavaScript for yourselves. Self XSS is a common tactic that scammers will use to take over your account. I have also only tested this on my Mint account and that of one other person, so no guarantees that this will work for you. If you understand the risks, here are the steps:

  1. Open an incognito tab in a Chrome or Chromium based browser (I havwe tested on Microsoft Edge).
  2. Login to your Mint account.
  3. From any page on Mint, open up the developer console by right clicking and selecting "Inspect" (or Ctrl + Shift + I).
  4. Find the "Console" tab.
  5. Paste the JavaScript code from the previous section into the console and press Enter.
  6. Look for messages such as Fetching account 1/88: Membership Share in the console to show up every 10-20 seconds, which show the progress of the script.
    1. If you need to abort the script, just reload the page.
  7. At the end, a message like Finished in 12.31635 minutes will be shown and mint_bal_history.csv will show up in your Downloads.

Update 2023-11-16: I noticed today that Monarch has built a browser plugin to download all the Mint data, including the daily balance history. That is probably a more robust option than what I posted here.