• Time to read 7 minutes
Building with an Open Bank API Part 1

Building with an Open Bank API Part 1

Banks have been exposing some of their most closely guarded functionalities as RESTful API services for developers to integrate with. In Singapore, it started with OCBC slightly more than a year ago, followed by Citibank. OCBC started with simple RESTful services to list ATM locations and products. Just 2 months ago, it launched a suite of APIs that allows developers to make API calls to read a user's accounts and transactions and for fund transfers between accounts. That's a big step forward because developers can now write apps that actually do something meaningful - like putting other people's money into their bank account.

Another bank that's well ahead of the pack is Citibank, which has made a whole suite of RESTful API services for payments, fund transfer, cards services and other meaningful services that you would otherwise only be able to do with an Internet banking system. This blog post documents my learning journey to build a web application that will allow a Citibank client to transfer money to an external account through several API calls. 

What you need

You will need to sign up for a developer's closed beta programme by Citibank. I received my account for close to a year before attempting this project. You will need to register for an account too if you wish to try my codes. It took them a few hours to get back to me with an approval to access their API running in Sandbox mode using simulated data.

Next, you need to run Node.JS. I run mine on GCloud. You could make API calls to Citi's API directly via a HTML+JavaScript frontend but youu will then expose your Citi developer's Client Secret Key since anyone could read your front-end JavaScript codes. Also, some of the calls have a really long list of parameters that you may just want to set as defaults. Because of this, I decided that I will wrap Citi's API calls with my own RESTful web services running on Node.JS.

Then you need to build a front-end web application to trigger these calls based on user's interaction. I developed this part in HTML+JavaScript using the jQuery library. To make my UI beautiful, I used Bootstrap. For this demonstrate, I will make calls to my own APIs using Postman.

My Application

Login to https://developer.citi.com/. Under "Applications", note your Client ID and Client Secret Key. Be careful about your Client Secret Key, because anyone who has your key could write a program and pretend that it is yours. 

api_myapp_0.png

Logging In

To use your app, users must be redirected back to Citi's sign-on page. I constructed this as a page with a [Login] button. On click, it makes a call to a function that forms a URL to redirect the user to a particular page that you have stated.

api_login_1_0.png

The user gets directed to Citi's login page where he is required to provide his login credentials.

api_login_2_0.png

The user is then informed of what your app could do to his account. Here, I turned on everything.

api_login_3_0.png

You may find index.html here and index.js here. Here's a sample of how the URL is formed:

https://sandbox.apihub.citi.com/gcb/api/authCode/oauth2/authorize?response_type=code&client_id=<MyClientID>&scope=pay_with_points accounts_details_transactions customers_profiles payees personal_domestic_transfers internal_domestic_transfers external_domestic_transfers bill_payments cards onboarding reference_data&countryCode=SG&businessCode=GCB&locale=en_SG&state=12093&redirect_uri=<my callback URL>

Citi's API documentation explains adequately what you need to pass. Here's a brief description:

  1. client_id: You can find this under your application in Citi's developer.citi.com page.
  2. scope: This is what your app is requesting your user to allow you to do. Here, I am asking for everything that Citi's API provides.
  3. redirect_uri: This is where your user will be redirected to upon successful login.

A successful login will provide your redirect_uri page with an authorization code. You will use the authorization code to request for an access token.

I developed my own RESTful web service called "authorize" to call Citi's API to exchange the access token by providing my authorization code. Citi's Authorization Code Grant API also returns a refresh token which you could use to re-request for a new access token if it expires. An access token expires every 1800 seconds. You will then need to request a new one using your refresh token. My Node.JS codes are here.

app.post('/authorize', function (req,res) {
	var response = [];
 	var client = new Client();

	if (typeof req.body.code !== 'undefined' && typeof req.body.state !== 'undefined' ){
		var code = req.body.code, state = req.body.state;
 
		//conversion to base64 because citi api wants it this way
    	var authorization = "Basic " + Buffer.from(client_id + ":" + client_secret).toString('base64');

    	var args = {
			data:{"grant_type":"authorization_code","code":code,"redirect_uri":callback},
			headers:{"Authorization":authorization,"Content-Type":"application/x-www-form-urlencoded"} 
		};

    	//get access and refresh token
    	var request = client.post("https://sandbox.apihub.citi.com/gcb/api/authCode/oauth2/token/sg/gcb", args, function (citidata, citiresponse) {
        	if (typeof citidata != 'undefined'){
            	console.log(citidata);
				res.setHeader('Content-Type', 'application/json');
    			res.status(200).send(JSON.stringify(citidata));
            }
		});
    
    	client.on('error', function(err) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'unauthorized access'}));
        });
  	} 
	else {
		res.setHeader('Content-Type', 'application/json');
    	res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'Please fill required details'}));
	}
});

api_get_account_0.png

Here, I printed the access token in my Node.JS console so that I could copy it to be used to call subsequent web services.

To test my web service codes for retrieving the access and refresh token, I used Postman

Get Deposits

With an access token, I am now able to make an API call to Citibank to retrieve all the accounts owned by this user. Citi's API to get accounts returns every type of accounts owned by this user such as deposits, investments and insurance. I am only interested in deposits, so I trimmed the dataset that the API returns to me to extract just these.

api_get_saving_0.png

app.post('/deposits', function (req,res) {
	var response = [];
 	var client = new Client();
	var myuuid = uuidv1();

	if (typeof req.body.access !== 'undefined'){
		var access = "Bearer " + req.body.access;
 
		var args = {
	    	headers:{ "Authorization":access,"uuid":myuuid,"Accept":"application/json","client_id":client_id} 
		};

		client.registerMethod("jsonMethod", "https://sandbox.apihub.citi.com/gcb/api/v1/accounts", "GET");
		client.methods.jsonMethod(args, function (citidata, citiresponse) {
        	var cloneObj;
        	if (typeof citidata != 'undefined'){
            	cloneObj = _.cloneDeep(citidata);

            	if (cloneObj.hasOwnProperty('accountGroupSummary')){
					cloneObj.accountGroupSummary= cloneObj.accountGroupSummary.filter(function(item) {
   						return item.accountGroup === 'SAVINGS_AND_INVESTMENTS';
					});
            
            		cloneObj = cloneObj.accountGroupSummary[0].accounts;
					res.setHeader('Content-Type', 'application/json');
    				res.status(200).send(JSON.stringify(cloneObj));
                }
            	else {
					res.setHeader('Content-Type', 'application/json');
    				res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'token expired'}));                	
                }
            }
		});

    	client.on('error', function(err) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'unauthorized access'}));
        });
  	} 
	else {
		res.setHeader('Content-Type', 'application/json');
    	res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'Please fill required details'}));
	}
});

Get Payees

And then I wish to know who are the people that this user has set up to make external fund transfers to. Citi does not allow prior fund transfer arrangements to be made in a 3rd party app like the ones I write. All fund transfer arrangements would have been made in Citi's own Internet banking portal. Our app could display these arrangements and this is taken care of by my payees web service. It displays all external payees so that the user could choose who to pay.

api_get_payee_0.png

app.post('/payees', function (req,res) {
	var response = [];
 	var client = new Client();
	var myuuid = uuidv1();

	if (typeof req.body.access !== 'undefined'){
		var access = "Bearer " + req.body.access;
 		var args = {
			headers:{ "Authorization":access,"uuid":myuuid,"Accept":"application/json","client_id":client_id} 
		};

    	client.registerMethod("jsonMethod", "https://sandbox.apihub.citi.com/gcb/api/v1/moneyMovement/payees?paymentType=EXTERNAL_DOMESTIC", "GET");
		client.methods.jsonMethod(args, function (citidata, citiresponse) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(200).send(JSON.stringify(citidata));
        });

    	client.on('error', function(err) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'unauthorized access'}));
        });
  	} 
	else {
		res.setHeader('Content-Type', 'application/json');
    	res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'Please fill required details'}));
	}
});

Buying Tokens

The next step is to perform the external fund transfer to a particular payee. I call this web service "Buy Token" because I had meant to integrate this with Go Dutch where users can transfer real money from one bank account to another and have it recorded as tokens of equal values in the Blockchain (more of this in a future post when I am done developing that).

api_buy_token_0.png

app.post('/buytoken', function (req,res) {
	var response = [];
 	var client = new Client();
	var myuuid = uuidv1();

	if (typeof req.body.access !== 'undefined' && typeof req.body.token !== 'undefined' && typeof req.body.accountId !== 'undefined' && typeof req.body.payeeId !== 'undefined'){
		var access = "Bearer " + req.body.access;
 		var token = req.body.token;
    	var accountId = req.body.accountId;
    	var payeeId = req.body.payeeId;
    
		//Assuming 1 token = SGD10
    	var amount = token * exchangerate;
    
		var args = {
    		data:	{"sourceAccountId":accountId,"transactionAmount":amount,"transferCurrencyIndicator":"SOURCE_ACCOUNT_CURRENCY","payeeId":payeeId,"chargeBearer":"BENEFICIARY","paymentMethod":"GIRO","fxDealReferenceNumber":"","remarks":"Fund Transfer","transferPurpose":"CREDIT_CARD_PAYMENT"},
    		headers:{ "Authorization":access,"uuid":myuuid,"Accept":"application/json","client_id":client_id,"Content-Type":"application/json" } 
		};

    	client.registerMethod("jsonMethod", "https://sandbox.apihub.citi.com/gcb/api/v1/moneyMovement/externalDomesticTransfer/preprocess", "POST");
		client.methods.jsonMethod(args, function (citidata, citiresponse) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(200).send(JSON.stringify(citidata));
		});
    
    	client.on('error', function(err) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'unauthorized access'}));
        });
  	} 
	else {
		res.setHeader('Content-Type', 'application/json');
    	res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'Please fill required details'}));
	}
});

Confirming Transfer

Fund transfer is a 2 step process. After performing the transfer, the user gets a controlFlowId that he will use to make a confirmation web service call to activate the transfer. Here's the code to do that.

 

api_confirm_buy_0.png

app.post('/confirmbuy', function (req,res) {
	var response = [];
 	var client = new Client();
	var myuuid = uuidv1();

	if (typeof req.body.access !== 'undefined' && typeof req.body.controlFlowId !== 'undefined'){
		var access = "Bearer " + req.body.access;
		var controlFlowId = req.body.controlFlowId;
    
    	var args = {
        	data:{"controlFlowId":controlFlowId}, 
        	headers:{ "Authorization":access,"uuid":myuuid,"Accept":"application/json","client_id":client_id,"Content-Type":"application/json" } 
        }; 
    	
    	client.registerMethod("jsonMethod", "https://sandbox.apihub.citi.com/gcb/api/v1/moneyMovement/externalDomesticTransfers", "POST"); 
    	client.methods.jsonMethod(args, function (citidata, citiresponse) { 
			res.setHeader('Content-Type', 'application/json');
    		res.status(200).send(JSON.stringify(citidata));
        }); 

    	client.on('error', function(err) {
			res.setHeader('Content-Type', 'application/json');
    		res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'unauthorized access'}));
        });
  	} 
	else {
		res.setHeader('Content-Type', 'application/json');
    	res.status(400).send(JSON.stringify({'result' : 'error', 'msg' : 'Please fill required details'}));
	}
});

In Summary

Here's a recap of the 4 wrapper RESTful web service that I have developed to make the corresponding calls to Citi's API:

  1. authorize: Provides an authorization code and then receives access token and refresh token from Citi.
  2. deposits: Provides access token, and receives a list of deposits and saving accounts.
  3. payees: Provides access token, and receives a list of payees that a user can fund transfer to.
  4. buytoken: Provides access token, the payee to pay, the amount of tokens to purchase and receives a controlFlowId.
  5. confirmbuy: Provides controlFlowId and a fund transfer between the user and his payee will be activated. You receive a transactionReferenceId which you can then store somewhere to prove that the transaction has been completed.

Next Step

Now that these steps work in Postman, the next step is to develop the app's Web UI. On to part 2!

Photo by rawpixel on Unsplash