A working example of the PSD2 OAuth2 flow using the bunq API, built with Python and FastAPI. Use this as a reference when integrating with bunq as a Payment Service Provider (PSP).
Note: You may need formal PSD2 certification to access user payment data in production. This code targets the bunq sandbox environment and is not production-ready as-is.
PSD2 (Payment Services Directive 2) is a European regulation that requires banks to open up their payment infrastructure to licensed third-party providers via secure APIs. As a PSD2 provider you can:
- Read account information — balances, transactions, payment history (with user consent)
- Initiate payments — create payments or payment requests on behalf of users
| Entity | Role |
|---|---|
| bunq | The bank. Holds financial records and exposes the API. |
| End user | A bunq user who grants your app access to their account via OAuth. |
| This app (PSD2 provider) | Acts as the OAuth client. Stores user access tokens and makes API calls on their behalf. |
The app consists of:
- A FastAPI server exposing endpoints that proxy bunq's API
- A SQLite database storing OAuth access tokens for each connected user
- An RSA key pair used for request signing (generated automatically on first run)
- A PSD2 certificate registered with bunq (generated by
create_psd2_user.sh)
chmod +x create_psd2_user.sh
./create_psd2_user.shThis script:
- Generates an RSA installation key pair and a self-signed PSD2 certificate
- Registers the certificate with bunq's sandbox API
- Creates a device server and session
- Prints your PSD2 API key (
token_value) to the console
Copy that API key — you'll need it in the next step.
In production you would store the full credential response (including user ID) and manage certificate rotation. For this sandbox demo only the API key is needed.
Open dependencies.py and set YOUR_API_KEY to the value printed by the script:
YOUR_API_KEY = "your_api_key_here"pip install -r requirements.txtuvicorn main:app --reloadOpen http://localhost:8000/setup_one_time in your browser. This only needs to be run once per API key.
It will:
- Initialize the local database
- Register an Installation with bunq (RSA public key exchange)
- Register a Device Server
- Create a Session
- Create an OAuth client
- Register the OAuth callback URL (
http://localhost:8000/callback) - Write a
.envfile with your OAuth credentials
After this completes, restart the server so it picks up the .env file.
Go to http://localhost:8000/auth. You'll be redirected to bunq's OAuth authorization page.
- Scan the QR code with the bunq sandbox app (or paste it into an Android emulator)
- Grant access to the bank accounts you want to share
- You'll be redirected back to
/callback
If running on localhost you may need to change
httpstohttpin the redirect URL after being sent back.
A successful callback returns:
{"message": "OAuth success", "new_user_id": 1}The new_user_id is the local database ID for this connected user.
Open http://localhost:8000/docs for the Swagger UI. All endpoints are listed there with example payloads.
Every endpoint that acts on a user's behalf follows the same pattern internally:
- Look up the user's OAuth access token from the local database
- Exchange it for a session token via
POST /session-server - Use the session token to make the API call to bunq
Example — fetching a user's accounts:
GET /user/1/accounts
→ fetches the monetary accounts for the bunq user connected as local user 1.
| Method | Endpoint | Description |
|---|---|---|
| GET | /auth |
Start OAuth authorization flow |
| GET | /callback |
Exchange authorization code for access token |
| GET | /oauth-clients |
List OAuth clients for the PSD2 user |
| GET | /oauth-client/{id}/callback-url |
List registered callback URLs |
| POST | /oauth-client/{id}/callback-url |
Add a new callback URL |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/{user_id}/ |
Get user profile |
| GET | /user/{user_id}/accounts |
List monetary accounts |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/{user_id}/payments/{account_id} |
List payments (paginated) |
| GET | /user/{user_id}/monetary-account/{account_id}/payment/{payment_id}/ |
Get a single payment |
| POST | /user/{user_id}/payment |
Create an immediate payment |
| GET | /user/{user_id}/monetary-account/{account_id}/draft-payment/{payment_id}/ |
Get a draft payment |
| POST | /user/{user_id}/draft-payment |
Create a draft payment |
| PUT | /user/{user_id}/monetary-account/{account_id}/draft-payment/{payment_id}/ |
Accept a draft payment |
| POST | /user/{user_id}/draft-payment-batch |
Create multiple draft payments |
| Method | Endpoint | Description |
|---|---|---|
| POST | /user/{user_id}/request-inquiry |
Create a payment request |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/{user_id}/cards |
List cards |
| GET | /user/{user_id}/card/{card_id} |
Get a specific card |
| POST | /user/{user_id}/credit-cards |
Order a new card |
| PUT | /user/{user_id}/card/{card_id} |
Update card settings |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/{user_id}/monetary-account/{account_id}/bunqme-tab/ |
List bunqme tabs |
| POST | /user/{user_id}/monetary-account/{account_id}/bunqme-tab/ |
Create a bunqme tab |
| Method | Endpoint | Description |
|---|---|---|
| POST | /user/{user_id}/monetary-account/{account_id}/attachment |
Upload a file attachment |
| POST | /user/{user_id}/monetary-account/{account_id}/payment/{payment_id}/note-attachment |
Link attachment to a payment |
| GET | /user/{user_id}/monetary-account/{account_id}/payment/{payment_id}/note-attachment |
Get attachments on a payment |
| POST | /user/{user_id}/monetary-account/{account_id}/payment/{payment_id}/note-text |
Add a text note to a payment |
| GET | /user/{user_id}/monetary-account/{account_id}/payment/{payment_id}/note-text |
Get text notes on a payment |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/{user_id}/notification-filter-url |
List URL notification filters |
| POST | /user/{user_id}/notification-filter-url |
Set URL notification filters |
| PUT | /user/{user_id}/notification-filter-url |
Update URL notification filters |
| GET | /user/{user_id}/notification-filter-failure |
List notification delivery failures |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/{user_id}/event |
List events |
| GET | /user/{user_id}/event/{item_id} |
Get a specific event |
| GET | /user/{user_id}/additional-transaction-information-category |
Get transaction info categories |
| GET | /user/{user_id}/mastercard_action/ |
List Mastercard actions |
| GET | /user/{user_id}/mastercard_action/{item_id} |
Get a specific Mastercard action |
| Method | Endpoint | Description |
|---|---|---|
| POST | /psd2/payment-service-provider-issuer-transaction |
Create a PSD2 issuer transaction |
| GET | /psd2/payment-service-provider-issuer-transaction/{transaction_id} |
Get a PSD2 issuer transaction |
| GET | /psd2/payment-service-provider-issuer-transaction-public/{public_id} |
Get public transaction info |
| GET | /psd2/redirect/{public_id} |
Redirect user to bunq PSP payment page |
| Method | Endpoint | Description |
|---|---|---|
| GET | /credential-password-ip |
List credential-password-ip objects |
| GET | /credential-password-ip/{ip_id} |
Get a specific one |
| GET | /credential-password-ip/{credential_id}/ip |
List IP whitelist entries |
| POST | /credential-password-ip/{credential_id}/ip |
Add IP to whitelist |
| GET | /credential-password-ip/{credential_id}/ip/{item_id} |
Get a specific whitelist entry |
| PUT | /credential-password-ip/{credential_id}/ip/{item_id} |
Update a whitelist entry |