You Need A Budget (YNAB) is a great tool for staying on top of your budget and financial game. It provides tons of integrations with banks to keep your real-world transactions in sync with your budget - none of these banks is available to use by South Africans though.
Luckily, Investec has introduced Programmable Banking which we can make use of, together with YNAB's API, to also achieve this functionality.
Let's take a look at how to do this using Node and scheduling the sync by making use of the services provided by Heroku. As a bonus, all of this will cost you nothing.
Get access to Investec's API
At the moment, Investec's Programmable Banking is only available on request, and to software developers. Simply follow up with your private banker to gain access to this functionality.
Once you have, navigate to the Programmable Banking overview, then click on Open API and take note of your Client ID and Secret. These will be used as environment variables later.
Get access to YNAB's API
YNAB makes use of Personal Access Tokens, so authentication is much more simple. Simply follow the steps in the Quick Start to get your own PAT.
Let's get set up
I've made all the code available on GitHub. I'll dive into the details at the end of this article. For now, let's take a look at how to get it running.
Set up the repo for development and get your account IDs
If you have cloned/forked the repo, then you must create a file in the root of this repo named .env
. This file will be used by https://www.npmjs.com/package/dotenv to pull in any variables that you defined there as environment variables, which you can then access via process.env
.
In this .env
file, add the following:
INVESTEC_API_ID=xxx
INVESTEC_API_SECRET=yyy
YNAB_PAT=zzz
(replace xxx, yyy, zzz with the values that you got earlier.)
Run yarn install
and then you can run yarn listAccounts
.
This will print a list of your accounts with Investec, followed by a map of your budgets with YNAB and the accounts that are within each of them.
In particular, take note of the budget ID reported for YNAB that you want to keep in sync with Investec, and add it to .env
:
YNAB_BUDGET_ID=aaa
Account IDs
The weirdest part about this solution is mapping your accounts from Investec to those that you have in YNAB. We could have done some name matching here, but I prefer IDs since they are (usually) permanent. As such, my solution is to create a map of Investec account IDs to YNAB account IDs that are stored in environment variables.
In practical terms, I have a Private Bank Account, PrimeSaver, and Instalment Sale Loan Account with Investec. In YNAB, I have a budget that contains accounts Investec, Investec Prime Saver, and Car Loan.
I want all transactions that happen on Private Bank Account (on Investec) to reflect in Investec (on YNAB). And similar for the other accounts.
Let's create this mapping:
Create an account ID map in environment variables
Take note of the account IDs listed earlier for Investec and YNAB, and add the following to your .env file:
ixxx=aaa-bbb
iyyy=bbb-ccc
izzz=ccc-ddd
In this case, xxx , yyy , and zzz are the account IDs reported for my accounts with Investec (note: they must be pre-pended with i in this file, since variables generally shouldn't be numbers and are simply not supported in some environments).
aaa-bbb , bbb-ccc , and ccc-ddd are the related account IDs reported by YNAB.
Your .env file should now look like this:
INVESTEC_API_ID=xxx
INVESTEC_API_SECRET=yyy
YNAB_PAT=zzz
YNAB_BUDGET_ID=aaa
ixxx=aaa-bbb
iyyy=bbb-ccc
izzz=ccc-ddd
You are now ready to start syncing!
Do a test run
If you have everything set up correctly, you can run yarn ynabSync . This will pull your transactions from the past 2 days from Investec, and send them to YNAB. Don't worry about duplicates, YNAB is smart and maps imported transactions to those that may have already been input manually. Otherwise, it creates the transaction. You can run this script multiple times, YNAB will handle the duplicates as expected. You can now open your YNAB app and inspect the transactions that got pushed into it from this script.
If you get no errors, you are clear to go ahead with putting this on Heroku, and continuously having your transactions synced from Investec to YNAB.
Deploying to Heroku
Now that you have your environment set up, let's get this running in the cloud so that you don't have to worry about keeping your laptop running 24/7.
Heroku is very pleasant for making use of in small, personal projects. As such, create an account or log in here.
Once signed in, create a new app. Set this app up to perform continuous deployment on the GitHub repo that your code will live in.
Once your deployment is done, Heroku will create a resource called web npm start . This is a nice feature of Heroku to create a task if you do not have a Procfile. You can just safely toggle this off.
Next, add the environment variables that you have in your .env file to Heroku. Go to Settings, and add them one-by-one to Config Vars.
Back to the Resources tab, under Add-ons, search for Heroku Scheduler and add it to your app.
Open the dashboard for Heroku Scheduler by clicking on the name of the add-on. Click on Add Job, select your schedule (I did Every 10 minutes), and input yarn ynabSyncfor the command to run.
And that's it!
Heroku will now launch a task every 10 minutes that will run the script to keep your YNAB in sync with your Investect transactions!
Caveat
Although the sync will run every 10 minutes (or however often you set it to), card transactions are still in a pending state for about 2 days. Unfortunately, the transactions API of Investect do not return these transactions yet as they are not cleared. This has the side-effect that the card transactions that you do take about 2 days before they show up in YNAB.
In the future, this can be addressed by implementing a similar code for Investec's programmable cards, but for now, this suffices.
Show me the code already!
The code is actually really simple. The most interesting part is authentication.
Authentication
To get a token from Investec, we need to authenticate ourselves with OAuth2. Although it sounds scary, this boils down to a POST request:
export const getInvestecToken = async () => {
const tokenResponse = await fetch(
"https://openapi.investec.com/identity/v2/oauth2/token",
{
method: "POST",
body:
"grant_type=client_credentials&client_id=" +
process.env.INVESTEC_API_ID +
"&client_secret=" +
process.env.INVESTEC_API_SECRET,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
const tokenJson = await tokenResponse.json();
return tokenJson.access_token;
};
Here we make use of node-fetch to make a call to Investec's API to receive a Bearer token that we can use in subsequent calls. In the body, we put a querystring containing our authentication parameters:
grant_type=client_credentials
means that we are requesting a token that'll give us access to resources that belong to us.client_id=process.env.INVESTEC_API_ID
gets the client ID that we stored as an environment variable, and passes it along as basically a usernameclient_secret=process.env.INVESTEC_API_SECRET
similarly gets and passes the client secret
YNAB has a simple authentication flow in the sense that it abstracts getting a Bearer token to a Personal Access Token, that doesn't expire. As such, we simply use this token in our calls to YNAB - exactly the same as we do with the token that we get from Investec.
Listing accounts
const list = async () => {
const token = await getInvestecToken();
const investecAccounts = await getInvestecAccounts(token);
const ynabBudgets = await getYnabBudgets();
const ynabAccounts = {};
for (const budget of ynabBudgets.data.budgets) {
const accounts = await getYnabAccounts(budget.id);
ynabAccounts[budget.id] = accounts.data.accounts.map((a) => ({
id: a.id,
name: a.name,
}));
}
console.log("===== Investec accounts =====");
console.log(JSON.stringify(investecAccounts, null, 2));
console.log("\n\n");
console.log("===== YNAB Accounts =====");
console.log(JSON.stringify(ynabAccounts, null, 2));
};
First, we sign in to Investec to get the token that we need. Next, we retrieve the list of accounts from Investec:
export const getInvestecAccounts = async (token) => {
const accountsResponse = await (
await fetch(`https://openapi.investec.com/za/pb/v1/accounts`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
).json();
return accountsResponse.data.accounts;
};
We then retrieve all the budgets for the related YNAB account:
export const getYnabBudgets = async () => {
const ynabResponse = await (
await fetch(`https://api.youneedabudget.com/v1/budgets`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.YNAB_PAT}`,
},
})
).json();
return ynabResponse;
};
For each budget then, we get the accounts under that budget:
export const getYnabAccounts = async (budgetId) => {
const ynabResponse = await (
await fetch(
`https://api.youneedabudget.com/v1/budgets/${budgetId}/accounts`,
{
method: "GET",
headers: { ...getBasicHeaders() },
},
)
).json();
return ynabResponse;
};
Performing the sync between Investec and YNAB
As above, we start with getting a token from Investec and getting the list of Investec accounts.
Next, we get the transactions of the past 3 days for each account:
const todayIsoString = new Date().toISOString().split("T")[0];
const twoDaysAgoIsoString = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
const ynabTransactions = [];
for (const acc of accounts) {
const transactions = await getInvestecTransactionsForAccount(
token,
acc.accountId,
twoDaysAgoIsoString,
todayIsoString,
);
ynabTransactions.push(
...transactions.map((t) => ({
account_id: process.env[`i${acc.accountId}`],
date: t.transactionDate,
amount: (t.type === "DEBIT" ? -1 : 1) * t.amount * 1000,
payee_name: t.description.slice(0, 50),
import_id: `${(t.type === "DEBIT" ? -1 : 1) * t.amount * 1000}:${
t.transactionDate
}:${t.postedOrder}`,
cleared: "cleared",
})),
);
}
export const getInvestecTransactionsForAccount = async (
token,
accountId,
dateFromIsoString,
dateToIsoString,
) => {
const transactionsResponse = await (
await fetch(
`https://openapi.investec.com/za/pb/v1/accounts/${accountId}/transactions?fromDate=${dateFromIsoString}&toDate=${dateToIsoString}`,
{
headers: {
...getBasicHeaders(token),
},
},
)
).json();
return transactionsResponse.data.transactions;
};
For each Investec account's transactions, we map this into an array of transactions that will be sent to YNAB in a batch call:
const ynabResponse = await sendTransactionsToYnab(ynabTransactions);
export const sendTransactionsToYnab = async (transactions) => {
const ynabResponse = await (
await fetch(
`https://api.youneedabudget.com/v1/budgets/${process.env.YNAB_BUDGET_ID}/transactions`,
{
method: "POST",
headers: { ...getBasicHeaders() },
body: JSON.stringify({ transactions }),
},
)
).json();
return ynabResponse;
};
And that's all there is to it!
This code is not very error safe though, so we can do a lot of improvements in that regard. However, it's a simple script, so it should suffice.
For anything else, feel free to dig through the GitHub repo.