Skip to content

Instantly share code, notes, and snippets.

@dimaj
Last active August 6, 2024 17:31
A small script that will attempt to parse transaction notification email and push it over to YNAB and

For this method to work, you need to:

  • Configure alerts from your banks to be sent to you via an email
  • Configure your Gmail with filters that will flag all of your alerts emails with a ynab-to-process label
  • Create a new google scripts project (https://script.google.com) with contents of this gist (make sure that you update all references to ynab-<bank>-account-id to your actual account ids as found in ynab)
  • Setup a trigger to run every however often you want it to run (I have mine every 10 minutes)

If you have more banks, simply add more entries to the accountsMap object and update regexes to match.

function getValueFromMatchAtIndex(match, index) {
if (match && match.length > index) {
return match[index];
}
}
function parseAmount(amountStr) {
return amountStr && parseFloat(amountStr.replace(',', '')) || undefined;
}
const accountsMap = {
chase: {
match2: (from) => from.indexOf('chase') >= 0,
fields: (body) => {
const accountMatch = body.match(/Account<\/td>(.|\s)+?>((.|\s)+?)</m);
const merchantMatch = body.match(/Merchant<\/td>(.|\s)+?>((.|\s)+?)</m);
const amountMatch = body.match(/Amount<\/td>(.|\s)+?>\$((.|\s)+?)</m);
const account = getValueFromMatchAtIndex(accountMatch, 2);
let merchant = getValueFromMatchAtIndex(merchantMatch, 2);
const amount = parseAmount(getValueFromMatchAtIndex(amountMatch, 2));
if (merchant.indexOf('AMZN') >= 0) {
merchant = 'Amazon';
}
const ynabAccount = (account && account.indexOf('1234') >= 0)
? { account: 'ynab-chase-account-id', name: 'Chase Fredom' }
: (account && account.indexOf('5678') >= 0)
? { account: 'ynab-amazon-account-id', name: 'Amazon Prime' }
: undefined;
return { ...ynabAccount, merchant, amount };
}
},
amazon: {
match2: (from) => from.indexOf('chase') >= 0
},
citi: {
match2: (from) => from.indexOf('citi') >= 0,
fields: (body) => {
const merchant = getValueFromMatchAtIndex(body.match(/Merchant<\/span>(.|\s)+?>\b((.|\s)*?)<\/span/m), 2);
const amount = parseAmount(getValueFromMatchAtIndex(body.match(/Amount(.|\s)+?\$(.|\s)*?([\d\,\.]*)</m), 3));
const account = 'ynab-city-account-id';
return { account, merchant, amount, name: 'Citi' }
}
}
}
function processEmails() {
Logger.log('Starting to process emails');
const toProcessLabelName = 'ynab-to-process';
const processedLabelName = 'ynab-processed';
const toProcessLabel = GmailApp.getUserLabelByName(toProcessLabelName);
const processedLabel = GmailApp.getUserLabelByName(processedLabelName) || GmailApp.createLabel(processedLabelName);
Logger.log(`Starting to iterate over ${toProcessLabel.getThreads().length} emails`);
toProcessLabel.getThreads().forEach(email => {
Logger.log('Starting on email')
const success = email
.getMessages()
.map(processEmailMessage)
.reduce((prev, cur) => prev && Math.round(cur.getResponseCode() / 100) === 2, true);
Logger.log('Finished email with status:', success);
if (success) {
email
.removeLabel(toProcessLabel)
.addLabel(processedLabel)
.moveToArchive()
.refresh();
} else {
Logger.log('Failed to process email thread');
}
});
}
function processEmailMessage(message) {
Logger.log('Starting to process message');
const accountName = Object.keys(accountsMap)
.filter(a => accountsMap[a].match2(message.getFrom()))
.shift();
if (!accountName) {
Logger.log(`Failed find account for email from ${message.getFrom()}`);
return false;
}
Logger.log(`Found account '${accountName}'`);
const account = accountsMap[accountName];
const fields = account.fields(message.getBody());
Logger.log('Fields are:', fields)
const transaction = {
budgetId: <ynab-budget-id>,
accountId: fields.account,
amount: fields.amount * -1,
payeeName: fields.merchant,
approved: false
};
Logger.log(transaction);
// submit ynab transaction
return enterTransaction(transaction);
}
function enterTransaction({ budgetId, accountId, amount, payeeName, approved = false, cleared } = transaction) {
var transactionData = {
transaction: {
account_id: accountId,
date: Utilities.formatDate(new Date(), "UTC", "yyyy-MM-dd"),
amount: parseInt(amount * 1000),
payee_name: payeeName,
memo: 'Entered automatically by Google Apps Script automation',
cleared,
approved
}
};
var options = {
method: 'POST',
payload: JSON.stringify(transactionData),
headers: headers
};
var transactionUrl = url + '/budgets/' + budgetId + '/transactions';
return UrlFetchApp.fetch(transactionUrl, options);
}
@cjrezz
Copy link

cjrezz commented Oct 23, 2022

Hey there. I could really use a little more detailed instructions on what exactly to modify in your script to have this work with my own bank account. Thanks!!

@dimaj
Copy link
Author

dimaj commented Oct 24, 2022

There are several things that have to happen to make this work:

  • Getting an API token from YNAB. (https://api.youneedabudget.com/)
  • Get your budgetId and account ids. This is done by opening up your YNAB page at an account of interest and looking at the URL. (e.g. if your URL is: https://app.youneedabudget.com//accounts/)
  • Come up with a regular expression that would extract fields like Merchant Name, Transaction Amount, anything else that you'd like to include in your transaction in YNAB.
    • Open up an email from your bank that contains your transaction
    • View email's HTML by clicking on 3 dots in the upper right corner of the email and selecting Show Original menu item
    • Copy HTML code and paste it into something like https://regex101.com/ (this is a site that helps you build out your regular expression and test them)
  • Once you have all that, update this code to match your details

Who do you bank with? I might have a set of regular expressions for you.

@cjrezz
Copy link

cjrezz commented Oct 31, 2022 via email

@dimaj
Copy link
Author

dimaj commented Nov 1, 2022

Sorry to hear that. Hope you're feeling better.

Unfortunately, I do not bank with them (I'm from the US), but if you'll send me the email you get from them, I might be able to help you with regular expressions to extract the pieces needed for the script.
NOTE: if you are going to post copy of the email, MAKE SURE YOU REMOVE ANY PERSONAL INFORMATION! (E.G. ACCOUNT NUMBER, EMAIL ADDRESSES, ETC)
You can also use a service such as https://pastebin.org and set an expiration date

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment