A first look at the SBanken Open Banking API
Due to the PSD2 directive and Open Banking initiatives, Norwegian banks have started to make their data available to external parties and customers through APIs. Several banks have now launched their first versions of APIs and developer portals. Nordea seems to be the one with the most functionality exposed (https://developer.nordeaopenbanking.com/), while DnB has organized several hackathons and are making an effort to build a community (https://community.dnb.no/t5/DNB-Developer/ct-p/Developer).
I'm a customer of SBanken and they recently launched their public API at https://sbanken.no/bruke/utviklerportalen/. In this post I'll document my attempt at using the API to retreive my data and hopefully transfer real funds between accounts.
Getting access to the API
These are the criteria and steps to get access to SBanken's API:
- You must have Bank ID
- You have to be more than 18 years old and a customer of SBanken
- Log on to SBanken and enable the beta program in your personal settings
- Complete the API setup wizard to get a client ID and password
The API setup wizard is straight-forward. Create an application with name and description to get a client ID.
Then sign the agreement using Bank ID and you will be presented with your client ID (Applikasjonsnøkkel). The client ID needs an accompanying client secret (passord). Create the secret by clicking "Bestill nytt passord".
Note that the combination of client ID and secret provides access to all your accounts and customer metadata. Do not give these to anyone or check them in with your source code. The secret expires after 3 months, which I find a bit comforting but also raises a lot of questions in regards to the practical use of the API. It seems like the only way to generate a secret is doing it manually, meaning this is not really viable in a real app scenario. I assume this will be improved in upcoming iterations.
Authenticating
With the client ID and secret in hand, let's have a look at what can be done with the API. SBanken uses OAuth2 & OpenID Connect for authentication and authorization and it seems to be implemented using IdentityServer.
This is an abbreviated version of the result from then OpenID discovery url at https://api.sbanken.no/identityserver/.well-known/openid-configuration:
{
"issuer": "https://api-localhost/identityserver",
{...},
"token_endpoint": "https://api.sbanken.no/identityserver/connect/token",
{...},
"scopes_supported": [
"CustomerIdentityResources",
"BatchUserIdentityResources",
"profile",
"openid",
"Bank.Accounts.full_access",
"Bank.Accounts.read_access",
"Bank.Payments.full_access",
"Bank.Payments.read_access",
"Bank.Transactions.full_access",
"Bank.Transactions.read_access",
"Bank.Transfers.full_access",
"Bank.Transfers.read_access",
"Customers.Customers.full_access",
"Customers.Customers.read_access",
"StatisticsAPI.full_access",
"IdentityServerManager.full_access",
"IdentityServerManager.read_access",
"Sandbox.DataManagers.full_access",
"Sandbox.DataManagers.read_access",
"Sandbox.StateManagers.full_access",
"Sandbox.StateManagers.read_access",
"Feeds.Feeds.full_access",
"Feeds.Feeds.read_access",
"Feeds.HotPicks.full_access",
"Feeds.HotPicks.read_access",
"Insurance.ConsumerLoan.full_access",
"Insurance.ConsumerLoan.read_access",
"BetaToggle.BetaToggle.full_access",
"BetaToggle.BetaToggle.read_access",
"BetaToggle.Products.full_access",
"BetaToggle.Products.read_access",
"Spare.Qpm.read_access",
"offline_access"
],
"claims_supported": [],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"implicit",
"Delegation"
],
{...},
}
That looks like a cookie jar of open banking goodness, but currently it seems the SBanken Beta API only supports the client credentials flow and a small subset of the scopes above.
To access the APIs we first need an access token. We can get one by passing the client ID and credentials to the token endpoint (these need to be passed as a basic auth header, meaning the string "Basic " combined with the client ID and secret separated by a colon, base64-encoded). The easiest way to generate the encoded string is by using btoa
in any browser console: btoa("<client_id>:<client_secret>")
.
I use Postman to send the requests, but any other http client would do. Make sure to specify the grant_type
in the body of the request.
Request
POST /identityserver/connect/token
Host: api.sbanken.no
Authorization: Basic (censored)
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
Response
{
"access_token": "(censored access token)",
"expires_in": 3600,
"token_type": "Bearer"
}
Note that the token expiry is 1 hour. By copying the token and decoding it on jwt.io I can see which scopes I've actually been given access to:
"scope": [
"Bank.Accounts.read_access",
"Bank.Transactions.read_access",
"Bank.Transfers.full_access",
"Customers.Customers.read_access"
]
A tiny subset of the available scopes, but still plenty to be able to have some fun (Bank.Transfers.full_access
👍). In the next section I will use the access token to make some simple API calls.
Making API calls
At the time of writing, the only endpoints available are these:
https://api.sbanken.no/Customers/swagger
https://api.sbanken.no/Bank/swagger
I'll start by retrieving my customer data using my SSN (fødselsnummer) and the access token
Request
GET /Customers/api/v1/Customers/{ssn}
Host: api.sbanken.no
Authorization: Bearer (access token goes here)
Accept: application/json
Response (abbreviated)
{
"item": {
"customerId": "(censored)",
"firstName": "Anders",
"lastName": "Austad",
"emailAddress": "post@andersaustad.com",
"dateOfBirth": "(censored)",
"postalAddress": {
"addressLine1": "(censored)",
"addressLine2": "",
"addressLine3": "0454 OSLO",
"addressLine4": "NORGE",
"country": "NO",
"zipCode": null,
"city": null
},
"streetAddress": { ... },
"phoneNumbers": [{ ... }]
},
{ ... }
}
Fine, but no surprises there. Next up is getting a list of accounts:
Request
GET /Bank/api/v1/Accounts/{ssn}
Host: api.sbanken.no
Authorization: Bearer (access token goes here)
Accept: application/json
Response (abbreviated)
{
"availableItems": 6,
"items": [
{
"accountNumber": "(censored)",
"customerId": "(censored)",
"ownerCustomerId": "(censored)",
"name": "Hovedkonto",
"accountType": "Standard account",
"available": (censored),
"balance": (censored),
"creditLimit": 0,
"defaultAccount": false
},
{
"accountNumber": "(censored)",
"customerId": "(censored)",
"ownerCustomerId": "(censored)",
"name": "Lønnskonto",
"accountType": "Standard account",
"available": (censored),
"balance": (censored),
"creditLimit": 0,
"defaultAccount": false
},
{...}
]
}
And then we can dig into the transation details for a single account:
Request
GET /Bank/api/v1/Transactions/{ssn}/{accountNumber}
Host: api.sbanken.no
Authorization: Bearer (access token goes here)
Accept: application/json
Response (abbreviated)
{
"availableItems": 22,
"items": [
{
"transactionId": "(censored)",
"customerId": "(censored)",
"accountNumber": "(censored)",
"otherAccountNumber": null,
"amount": -(censored),
"text": "Til: Monner AS Betalt: 23.01.18",
"transactionType": "RKI",
"registrationDate": null,
"accountingDate": "2018-01-24T00:00:00+01:00",
"interestDate": "2018-01-24T00:00:00+01:00"
},
{
"transactionId": "(censored)",
"customerId": "(censored)",
"accountNumber": "(censored)",
"otherAccountNumber": null,
"amount": (censored),
"text": "Til: HAFSLUND TELLIE Betalt: 17.01.18",
"transactionType": "RKI",
"registrationDate": null,
"accountingDate": "2018-01-17T00:00:00+01:00",
"interestDate": "2018-01-17T00:00:00+01:00"
},
{...}
}
Finally, let's transfer money from one account to another. This one is a bit trickier, as the documentation is lacking and has errors - but I got it working by posting a request like this:
Request
POST /Bank/api/v1/Transfers/{ssn}
Host: api.sbanken.no
Accept: application/json
Authorization: Bearer (access token goes here)
Content-Type: application/json
{
"FromAccount": "(censored)",
"ToAccount": "(censored)",
"Amount": 50.0,
"Message": "Testing the API"
}
Response
If the transfer is successfull, http code 200 is returned with an empty body.
Funnily enough, the Swagger docs state that the
Amount
"Must be equal to or greater than 1.00 and less than 100000000000000000.00.". Good thing they warned about that upper limit...
I can now re-check the transaction and see that, indeed, the money has been transferred! 🎉
{
"transactionId": "0",
"customerId": "(censored)",
"accountNumber": "(censored)",
"otherAccountNumber": null,
"amount": 50,
"text": "Testing the API",
"transactionType": "Overføring",
"registrationDate": "2018-01-27T00:00:00+01:00",
"accountingDate": "2018-01-27T00:00:00+01:00",
"interestDate": "2018-01-27T00:00:00+01:00"
}
And this is how it looks in the actual online bank:
Wrapping up
It's easy to get started with SBanken's online banking API. The authentication scheme and API is based on existing standards well-known to most developers. The API is at a very early stage and thus limited to only basic functionality, but there's definitely a potential here for some creative and useful apps.
I hope SBanken will make good use of their github account at https://github.com/Sbanken, allowing the community to contribute with corrections, documentation and responding to filed issues (someone has already submitted a PR for a Java example!). The documentation is lacking, but I find the API examples easy to understand. To make the barrier of entry even lower, SBanken could provide a Postman collection with all available requests as well as improving their Swagger documentation.
Final note: I'm considering creating a .NET Standard client for the SBanken API. Please let me know if this would be of interest. A .NET client wrapping the API is now available as a nuget and source code is on github. I would be happy to discuss any open banking ideas you may have, feel free to leave a comment or contact me on @anderaus.