Building a simple PHP application using the bunq API (ft. PSD2)
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:
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):
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:
- The authorization request URI. In the production environment, this is
https://oauth.bunq.com/auth
, in the sandbox environment, it’shttps://oauth.sandbox.bunq.com/auth
.
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.php
under the first handler (should be around line 17):
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:
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
- Retrieve the generated
code
and thestate
from the query string. - Verify that the
state
equals the state we saved into the session of the user before. - Exchange the
code
we’ve just got for anaccess_token
, which can be used to access the bunq API. - 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 theaccess_token
).
Let’s take a look at the first part of the endpoint handler:
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:
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
alwaysauthorization_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:
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):
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
— eitherBunqEnumApiEnvironmentType::SANDBOX()
orBunqEnumApiEnvironmentType::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
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:
- Check that the user is authenticated.
- Deserialize the APIContext from the session.
- Perform the API request(s) using the SDK.
- 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.
Put this somewhere (e.g. in src/routes.php
) where you can access it when defining your routes, like the dashboard route:file
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.
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):
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:
with an even uglier view (templates/transactions.html.twig
):
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:
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):
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.
Hope you liked my tutorial! If so, don’t forget to cap and comment 😀