Building a simple PHP application using the bunq API (ft. PSD2)

Jan Beckmann
bunq Developers’ Corner
12 min readJul 1, 2019

Besides revolutionizing banking with its exceptionally good app and excellent customer service, bunq has built one of the most amazing banking APIs. Let’s explore this API by creating a simple web app for your bunq account.

The aim of this tutorial

You are going to build a simple PHP-based web app that will allow you to check your current account balance and view your most recent transactions. You will be able to log into this app via OAuth and see an interface that looks approximately like this:

The bunq web app we are going to create

What this tutorial won’t cover

To keep it short and simple, I have omitted some aspects that you should take care of before bringing your code to anywhere near production:

  • Error handling
  • Testing
  • Splitting up your code (just for the sake of comprehensiveness, we are going to do most work in src/routes.php)
  • Logging out / expiring sessions

Requirements

  • Basic PHP knowledge
  • Composer
  • The bunq sandbox app with an activated sandbox account (read here how to get these)
  • Basic knowledge of OAuth is advantageous, but not required. You can find a lot about it on the internet, so I’m not going to go into detail about the concepts behind OAuth.

Setup

To speed up the development, I decided to use a PHP framework, Slim. You can use any other (or no) framework instead; however, Slim seems like the right mix of simplicity and power to me.

1. Create a new project based on the Slim Skeleton:

$ composer create-project slim/slim-skeleton slim-bunq

You can replace slim-bunq with a project name of your choice

This will create a new directory called slim-bunq containing a simple Slim project.

As you are going to use Twig templates later instead of the pre-configured PHP-Views, you’ll have to tweak this a bit.

2. Execute these commands in your working directory:

$ composer remove slim/php-view
$ composer slim/twig-view

3. Replace the setup code

Change return new \Slim\View\PhpRenderer($settings['template_path']); in the src/dependencies.php file for the following code snippet (which is stolen from the Slim tutorial):

Twig setup for Slim

You can already run your new application using this command:

$ php -S localhost:8080 -t public public/index.php

Authentication with OAuth

bunq provides an OAuth server/workflow for accessing their API, which we will use to connect with the bunq users. Thanks to OAuth, a user only needs to scan a QR code on a bunq web tab to allow your application to send requests to the API on their behalf.

To begin using OAuth, you need the following:

1. An endpoint initiating the OAuth process

It is also called a login endpoint. In this tutorial application, it is accessible under /login. This endpoint redirects the user to the bunq app, where they either approve or disapprove your authorization request.

2. A callback endpoint

It receives the generated authorization code after the user has granted your app permission to access their account. Let’s provide this endpoint under /auth-callback . It will verify that everything is alright and will then persist the necessary authentication information in the user’s session.

3. An OAuth client.

On the production, you can do it in the bunq app (Security & Settings -> Developers).

For the sandbox environment, you can use my bunq API Toolbox 😃. Just enter your redirect URIs (i.e. http://localhost:8080/auth-callback) and click twice on “Generate”.

Please note: the toolbox is still in a pre-alpha state, i.e., almost untested and not very stable.

It’s also possible to use the bunq sandbox app (see above) to register an OAuth client.

4. A Client ID and a Client Secret

You get them after generating an OAuth client. In the toolbox, these will be the first two returned codes after you click on the right “Generate” button. Please make a note of both, as we will need them later.

Now that we know what we need to start with the bunq OAuth, let’s get it.

Creating the login endpoint

We are going to build the URI to redirect the user to out of multiple variables:

And, all as GET parameters:

  • the response_type — it’s always code as bunq only supports this type of authorization;
  • the client_id — it’s the Client ID you got above while registering our OAuth Client;
  • the redirect_uri http://localhost:8080/auth-callback in our case;
  • the state — a random (unique) string to verify that the request really originated from your site when your callback endpoint is called (this helps to prevent CSRF attacks).

Let’s insert the following snippet in our src/routes.phpunder the first handler (should be around line 17):

The login handler

Some notes about this code:

In lines two and three, you are generating the callback URL from our base URL and the URL for the route auth-callback , which is not yet there.

generateRandomString is a custom function which returns a random string (shocking, isn’t it? 😛). It’s stolen from StackOverflow and will be specified later.

$bunq_settings is an associative array containing some configuration variables to avoid hard-coding everything. We’ll look at this at a later point.

http_build_query is a built-in function, that generates a query string from an associative array.

You save the generated state into the user’s session to be able to verify it when he comes back.

After you add the missing implementations (so not yet — I’m sorry), you will be able to access http://localhost:8080/login in the browser redirecting you to a webpage like this:

bunq OAuth dialog

When the user scans this code in the bunq App, they are redirected to the callback endpoint, which is not there yet. You can test scanning the code using the bunq sandbox app.

Creating a callback endpoint

  1. Retrieve the generated code and the state from the query string.
  2. Verify that the state equals the state we saved into the session of the user before.
  3. Exchange the code we’ve just got for an access_token, which can be used to access the bunq API.
  4. Generate a new “API Context” (register a new device in the user’s account and create a new session). Don’t worry about this too much, the bunq PHP SDK does this for you.

Before you start, you need to require some additional composer modules:

$ composer require guzzlehttp/guzzle:~6.0
$ composer require bunq/sdk_php

Guzzle “is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services.” You’ll use it do step 3 (exchanging the code for the access_token ).

Let’s take a look at the first part of the endpoint handler:

The callback handler — Part I

As mentioned above, we are reading some code and state from the query string and validating it. We are also calculating the callback URL again.

Slim does not provide a simple way to access the current URL without the query string — at least I’m not aware of one — so I just copied the code from above.

The second part, covering just step 3, consists of only one HTTP request:

The callback handler — Part II

This uses the Client from Guzzle, requiring you to import the namespace first. To do so, insert

use GuzzleHttp\Client;

at the top of your file.

You are calling the token endpoint (https://api.oauth.bunq.com/v1/token in the production environment, https://api-oauth.sandbox.bunq.com/v1/token in the sandbox) to get an access_token for our code . Besides this code, you are also passing as GET these parameters:

  • grant_type always authorization_code as bunq only supports this at the moment;
  • redirect_uri — your redirect URI;
  • client_id — the Client ID of your OAuth client;
  • client_secret — the Client Secret of our OAuth client.

You also have to send a custom header (X-Bunq-Client-Request-Id) with a unique string (doesn’t have to be 16 chars long, doesn’t have to be random) for the bunq API to accept your request.

Please note: a lot of errors can happen here, so better do some proper error handling instead of assuming everything went fine.

Our last and third past is yet another API call, but this time more hidden:

The callback handler — Part III

generateAPIContext is a custom function, which will be unveiled soon (along with generateRandomString ). It returns a bunq API context. You are then serializing it into JSON and saving it into the user’s session, along with the access token you got in part 2.

Finally, you are redirecting the user to our root page where you will be able to do some great stuff using the API access you’ve just got.

There are still some things missing before we can move on. Let me just give you the promised code of the helper functions (just copy them to the bottom of the src/routes.php where you already copied all the other stuff):

Helper functions

The generateRandomString is nothing special, so let’s look at generateAPIContext. You’ll notice that there are some things you can configure before actually creating the API context:

  • The environmentType — either BunqEnumApiEnvironmentType::SANDBOX() or BunqEnumApiEnvironmentType::PRODUCTION() . To keep your app more flexible, you might consider moving this to your app settings.
  • A name for your “device” — in the bunq world, every client of the bunq API has to be a “device” identified by a description. Each device is bound to specific IP addresses. We are not using this feature here (the empty array of $permittedIPs equals a wildcard), but you could think about this if have a server with a static IP running your app.

The object you will receive from ApiContext::create contains a wide variety of things needed for accessing the API, including but not limited to, an RSA public / private key-pair.

If you used the bunq API toolbox for creating the OAuth client, you have already had a quick look at all the things needed here. Please try to reuse the API Context as much as possible to reduce the load on both your and bunq’s side.

That’s why we saved it in the session variable. 😉

Please note that you maybe need to add additional use Statements for the code to work properly:

use bunq\Util\BunqEnumApiEnvironmentType;                   
use bunq\Context\ApiContext;

Finally, I introduced the use of some setting variables above to reduce the need for hard-coding (sensitive) things like your Client ID and Client Secret. To make this usable, add

$bunq_settings = $container->get('settings')['bunq'];

just below the $container = $app->getContainer() line in your src/routes.php and

bunq Settings in src/settings.php

as an entry into the associative settings array in src/settings.php. I decided to put the client credentials into the environment variables, so you’ll have to add yet another composer module:

$ composer require vlucas/phpdotenv

Create a new file called .env in your root project folder (the folder containing composer.json ) with the following contents:

BUNQ_CLIENT_ID=YOUR_CLIENT_ID_HERE
BUNQ_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE

and load it before creating instantiating your app in public/index.php:

$dotenv = Dotenv\Dotenv::create(__DIR__ . '/../');                       $dotenv->load();

Now you should be all set! You can find the current state of our project here on GitHub.

Using the bunq API

Once you obtain an access_token, you can use the bunq API. Please note that there are some restrictions when using a token obtained using OAuth. Please refer to the bunq API docs for up-to-date-information on what actions you can perform.

As we are building a web app, the flow when accessing the bunq is always similar:

  1. Check that the user is authenticated.
  2. Deserialize the APIContext from the session.
  3. Perform the API request(s) using the SDK.
  4. Serialize the responses and pass them to the view/template engine.

We need to build a small middleware for step 1 to redirect unauthenticated users.

Middleware for redirecting non-authenticated users

Put this somewhere (e.g. in src/routes.php) where you can access it when defining your routes, like the dashboard route:file

Dashboard route

You will recognize the remaining three steps in this piece of code. Here, we are making two API requests:

  • Getting the “Main Monetary Account” of the currently logged-in user
  • Getting the avatar for this account

You can read more about these endpoints in the bunq API documentation. As the bunq SDK returns objects, we have to use the good old json_decode(json_encode()) trick to transform these into associative arrays (including the private attributes), which we then pass to Twig.

As we only get a UUID of the avatar and not the avatar itself when reading the account details, we have to go another step to retrieve the image contents, which we then pass base64 encoded to Twig.

Views

I’m not a good UI designer, so won’t comment on the following template files. Fortunately, they are quite self-explanatory and perfectly relevant to display the data we get from the API.

  • templates/base.html.twig is our base layout file, base.html.twig.
base.html.twig template
  • templates/dashboard.html.twig extends this file and displays just some basic information about the account (you can see it in action at the top of this article):
dashboard.html.twig template

You can now add just another route to display all the transactions. This basically works the same the way as the dashboard route, just using other endpoints:

Transaction route endpoint

with an even uglier view (templates/transactions.html.twig):

transactions.html.twig template

Fun fact: To generate transactions in the sandbox environment, just request money from / send money to sugardaddy@bunq.com!

And that’s it. You’ve just created a simple bunq web app.

I’ve published the whole code on GitHub. It contains a better index view including a button pointing to the login URL.

Don’t forget there are still things to do before you can bring this into production:

  • Add more features. I’ve listed all the features you can use with an OAuth access_token above and you probably want to implement all these
  • Provide better error handling
  • Create a better interface, including a way to log-in/ logout
  • PSD2 (see below)
  • Provide the notorious cookie consent banner as you use cookies (the PHP session cookie)

Happy coding! 😊

Bonus: PSD2

You may have heard about the new Payment Services Directive v2 (PSD2). The PSD2 has a lot of great things included, one of them being an easier and more secure way for third-party-providers to access your bank account.

Currently, a bank account holder often has to provide their online banking credentials to third-parties for them to access their account or execute payments (not when using bunq — they use OAuth). Worst-case scenario, the third party uses screen scraping to achieve this (not when using bunq — they provide a REST API).

Basically, PSD2 classifies third-party providers into three categories:

  • Payment Initiation Service Providers (PISP);
  • Account Information Service Providers (AISP);
  • and Card Based Payment Instrument Issuers (CBPII).

There are virtually no cases of use of the CBPII role so it’s less documented. Nevertheless, the bunq API supports it too.

To act as either one you need to get a certificate issued by a trusted authority, which can be quite difficult. You can read more about PSD2 here and on bunq Together.

However, for our purposes (just demonstrating how it could work) we don’t need to care too much about these points: bunq allows us to use self-generated certificates in a special sandbox-environment. And bunq allows us to basically just use its REST API after we’ve registered as PISP / AISP. So let’s get started.

Getting a certificate and registering a service provider

You can use the openssl command to generate a sandbox certificate:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=My App PISP AISP/C=NL'

Replace “My App” with the name of your app and “NL” with your country. If you only want to be either a PISP or an AISP just omit the other one

Alternatively, my bunq API Toolbox provides a way to generate this certificate as well. Additionally, it handles all the other steps required to register as a service provider as well:

Service provider registration flow. Image: bunq API docs

I’m assuming you used my API toolbox.

So after you’ve registered as a service provider, you need to create an OAuth client for the service provider.

Hint: my toolbox already did that for you.

Having done that, you can use the normal OAuth flow outlined above (the image below is kind of misleading: the first two steps — registering an OAuth client is only needed once, the steps after that — redirecting the user, etc. — are necessary each time a new user wants to use your app):

OAuth registration flow, Image: bunq API docs

So, all in all, to switch from “normal” OAuth to PSD2-compliant OAuth you just need to press some buttons in the bunq API toolbox and replace your OAuth client id/secret. Easy, right? Ok, two drawbacks:

  • This only works in the sandbox environment.
  • The information you can access as an AISP / PISP isn’t the same as the information you can access when using the standard OAuth flow outlined above. You will notice this when you try to use your new generated OAuth credentials with your old application from above — you won’t be able to access the avatar information. The bunq API docs give you a good overview of your permissions as AISP / PISP.
OAuth screen when authorizing an AISP / PISP

Hope you liked my tutorial! If so, don’t forget to cap and comment 😀

--

--