- Table of contents with anchor links - Ansible deploy.yml: clones repo, copies private key, bootstraps store.json via Ansible Vault variables, builds image, runs container; uses creates: guard to preserve live requisitions on re-deploy - AI-generated disclaimer (prominent, at the bottom) - Full MIT licence text (not just the word "MIT") - Improved prose and formatting throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|---|---|---|
| src | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package.json | ||
| README.md | ||
actual-gocardless-proxy
AI-generated code — see disclaimer below.
A drop-in GoCardless Bank Account Data API proxy that lets Actual Budget sync with French (and other European) bank accounts via Enable Banking — a free PSD2 aggregator.
Zero modifications to actual-server. The only change required is one environment variable:
GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
Table of contents
- How it works
- Setup
- Ansible deployment
- OAuth redirect flow
- Supported banks
- Troubleshooting
- Data & privacy
- Disclaimer
- License
How it works
Actual Budget (browser)
│ GoCardless API calls
▼
actual-server
│ GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
▼
gocardless-proxy ◄── this repo
│ Enable Banking PSD2 API (RS256 JWT auth)
▼
Enable Banking → French banks (BNP, SG, CA, LCL, BP, …)
The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly. All state (tokens, requisitions, account mappings) is persisted in ./data/store.json so Docker restarts do not break existing Actual Budget account links.
Setup
1. Sign up for Enable Banking
Go to enablebanking.com and create a free personal account. Under your application settings, note your Application ID.
2. Generate an RSA key pair
Enable Banking authenticates API calls with an RS256 JWT signed by your application's private key. You upload the public key to Enable Banking; the private key stays on your server.
# Generate a PKCS8 RSA private key
openssl genpkey -algorithm RSA -pkcs8 -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# Extract the public key — upload this to the Enable Banking dashboard
openssl pkey -in private_key.pem -pubout -out public_key.pem
Store private_key.pem securely. You will reference it during setup.
3. Run the setup script
git clone https://github.com/yourname/actual-gocardless-proxy
cd actual-gocardless-proxy
npm install
npm run setup
The script prompts for:
| Prompt | Example |
|---|---|
| Enable Banking Application ID | abc123-... |
| Path to private key PEM | /home/user/private_key.pem |
| Proxy base URL | https://yourserver.example.com or http://localhost:3456 |
The proxy base URL is the address Enable Banking redirects to after OAuth — it must be reachable from the user's browser, not just from within Docker.
On completion the script prints:
──────────────────────────────────────────────────────────
Enter these values in Actual Budget → Settings → GoCardless:
Secret ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
And set this environment variable on actual-server:
GOCARDLESS_BASE_URL=http://gocardless-proxy:3456
──────────────────────────────────────────────────────────
All credentials are written to ./data/store.json.
4. Add the proxy to docker-compose
Paste the gocardless-proxy service block into your existing actual-server docker-compose.yml and add GOCARDLESS_BASE_URL to actual-server:
services:
actual-server:
image: actualbudget/actual-server:latest
environment:
GOCARDLESS_BASE_URL: http://gocardless-proxy:3456
# ... rest of your actual-server env vars
depends_on:
- gocardless-proxy
gocardless-proxy:
build: /path/to/actual-gocardless-proxy
# or use a pre-built image:
# image: ghcr.io/yourname/actual-gocardless-proxy:latest
container_name: gocardless-proxy
restart: unless-stopped
volumes:
- /path/to/actual-gocardless-proxy/data:/app/data
environment:
PORT: 3456
Changing the proxy base URL later: Re-run
npm run setup, or editconfig.proxy_base_urldirectly indata/store.jsonand restart the container.
5. Start the services
docker compose up -d
Verify the proxy is up:
curl http://localhost:3456/health
# {"status":"ok","configured":true}
6. Configure Actual Budget
Go to Settings → GoCardless in Actual Budget and enter the Secret ID and Secret Key printed in step 3. Then link your bank via Accounts → Link account.
Ansible deployment
The playbook below deploys the proxy to a remote host that already runs actual-server via Docker Compose. Adjust variables at the top to match your environment.
# deploy.yml
---
- name: Deploy actual-gocardless-proxy
hosts: actual_host
become: true
vars:
proxy_dir: /opt/actual-gocardless-proxy
data_dir: /opt/actual-gocardless-proxy/data
proxy_port: 3456
# Set proxy_base_url to the URL reachable from the user's browser
proxy_base_url: "https://actual.example.com"
eb_app_id: "{{ vault_eb_app_id }}"
eb_private_key_src: "{{ playbook_dir }}/files/private_key.pem"
gc_secret_id: "{{ vault_gc_secret_id }}"
gc_secret_key: "{{ vault_gc_secret_key }}"
tasks:
- name: Install git and Docker dependencies
ansible.builtin.package:
name:
- git
- python3-docker
state: present
- name: Clone / update repository
ansible.builtin.git:
repo: https://github.com/yourname/actual-gocardless-proxy.git
dest: "{{ proxy_dir }}"
version: main
force: true
- name: Create data directory
ansible.builtin.file:
path: "{{ data_dir }}"
state: directory
mode: "0700"
- name: Copy RSA private key
ansible.builtin.copy:
src: "{{ eb_private_key_src }}"
dest: "{{ data_dir }}/private_key.pem"
mode: "0600"
- name: Write store.json (credentials + proxy config)
ansible.builtin.copy:
dest: "{{ data_dir }}/store.json"
mode: "0600"
content: |
{
"config": {
"eb_app_id": "{{ eb_app_id }}",
"eb_private_key": {{ lookup('file', eb_private_key_src) | to_json }},
"gc_secret_id": "{{ gc_secret_id }}",
"gc_secret_key": "{{ gc_secret_key }}",
"proxy_base_url": "{{ proxy_base_url }}"
},
"gc_tokens": {},
"institutions_cache": null,
"institutions_cache_at": 0,
"agreements": {},
"requisitions": {},
"accounts": {}
}
# Skip this task if store.json already exists (preserves live state:
# tokens, requisitions, linked accounts). Remove the condition if you
# want to reset credentials on every deploy.
args:
creates: "{{ data_dir }}/store.json"
- name: Build proxy Docker image
community.docker.docker_image:
name: actual-gocardless-proxy
tag: local
build:
path: "{{ proxy_dir }}"
source: build
force_source: true
- name: Run gocardless-proxy container
community.docker.docker_container:
name: gocardless-proxy
image: actual-gocardless-proxy:local
restart_policy: unless-stopped
ports:
- "127.0.0.1:{{ proxy_port }}:{{ proxy_port }}"
volumes:
- "{{ data_dir }}:/app/data"
env:
PORT: "{{ proxy_port | string }}"
STORE_PATH: /app/data/store.json
- name: Ensure actual-server has GOCARDLESS_BASE_URL set
ansible.builtin.debug:
msg: >
Remember to add
GOCARDLESS_BASE_URL=http://gocardless-proxy:{{ proxy_port }}
to your actual-server environment and restart it.
Sensitive variables (eb_app_id, gc_secret_id, gc_secret_key) should be stored in an Ansible Vault file:
# Create vault file
ansible-vault create group_vars/actual_host/vault.yml
# group_vars/actual_host/vault.yml
vault_eb_app_id: "your-enable-banking-app-id"
vault_gc_secret_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
vault_gc_secret_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Run the playbook:
ansible-playbook deploy.yml --ask-vault-pass
First deploy only:
store.jsonis written withcreates:so it is not overwritten on subsequent runs — this preserves live requisitions and linked accounts. On a brand-new host the credentials are bootstrapped automatically; no need to runnpm run setupmanually.
OAuth redirect flow
1. actual-server POST /api/v2/requisitions/
body: { redirect: "http://actual:5006/...", institution_id }
↓
2. proxy POST https://api.enablebanking.com/auth
{ aspsp: { name, country },
redirect_url: "http://proxy/callback",
state: <requisition_id> }
← { url: "https://tilisy.enablebanking.com/..." }
↓
3. proxy returns requisition { id, status: "CR", link: <EB auth URL> }
↓
4. User visits the link, authenticates at their bank
↓
5. Enable Banking → http://proxy/callback?code=xxx&state=<requisition_id>
↓
6. proxy POST https://api.enablebanking.com/sessions { code }
← { session_id, accounts: [uid, ...] }
→ fetches account details per UID
→ stores gc_id ↔ eb_uid mapping in store.json
→ sets requisition status "LN" (linked)
→ 302 → http://actual:5006/...?requisition_id=<req_id>
↓
7. actual-server GET /api/v2/requisitions/<req_id>/
← { status: "LN", accounts: ["gc-uuid-1", ...] }
↓
8. actual-server GET /api/v2/accounts/<id>/details/
GET /api/v2/accounts/<id>/balances/
GET /api/v2/accounts/<id>/transactions/
Supported banks
All French banks supported by Enable Banking, including:
| Bank | BIC |
|---|---|
| BNP Paribas | BNPAFRPP |
| Société Générale | SOGEFRPP |
| Crédit Agricole | AGRIFRPP |
| LCL | CRLYFRPP |
| Banque Populaire | various |
| Caisse d'Épargne | various |
| La Banque Postale | PSSTFRPPPAR |
| Crédit Mutuel | CMCIFRPP |
| CIC | CMCIFRPP |
| Boursorama | BOUSFRPPXXX |
| Fortuneo | FTNOFRP1 |
| Hello bank! | BNPAFRPP |
See enablebanking.com/docs/markets/fr/ for the complete list.
Troubleshooting
"Proxy not configured" error
Run npm run setup and restart the container.
"Unknown institution_id" when linking an account actual-server caches the institutions list. Go to Settings → GoCardless → Reset and re-fetch.
OAuth callback fails with "Enable Banking error"
proxy_base_urlindata/store.jsonmust be reachable from your browser, not just from inside Docker.- Make sure the callback URL (
{proxy_base_url}/callback) is whitelisted in your Enable Banking application settings.
Transactions not appearing Enable Banking may return an empty array if no transactions fall in the requested date range. Try widening the range in Actual Budget.
Private key parse error The key must be in PKCS8 format. To convert from a traditional RSA key:
openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem
Data & privacy
All credentials and session data are stored locally in ./data/store.json. No data is sent to any third party other than Enable Banking (the licensed PSD2 provider) and your bank.
The data/ directory is git-ignored and must not be committed.
Disclaimer
This project was entirely generated by an AI assistant (Claude, by Anthropic) without manual code authorship.
It has not been independently audited, penetration-tested, or validated against production bank APIs. Use it at your own risk. In particular:
- The Enable Banking integration is based on publicly available documentation and may require adjustments as the API evolves.
- No warranty is given regarding correctness, security, or fitness for any purpose.
- Your bank credentials and PSD2 access tokens are handled by this software — review the code before deploying in a sensitive environment.
Contributions, corrections, and reviews from human developers are very welcome.
License
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.