# actual-gocardless-proxy > **AI-generated code — see disclaimer below.** A drop-in [GoCardless Bank Account Data API](https://developer.gocardless.com/bank-account-data/quick-start-guide/) proxy that lets [Actual Budget](https://actualbudget.org/) sync with French (and other European) bank accounts via [Enable Banking](https://enablebanking.com/) — 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 1. [How it works](#how-it-works) 2. [Setup](#setup) - [1. Sign up for Enable Banking](#1-sign-up-for-enable-banking) - [2. Generate an RSA key pair](#2-generate-an-rsa-key-pair) - [3. Run the setup script](#3-run-the-setup-script) - [4. Add the proxy to docker-compose](#4-add-the-proxy-to-docker-compose) - [5. Start the services](#5-start-the-services) - [6. Configure Actual Budget](#6-configure-actual-budget) 3. [Ansible deployment](#ansible-deployment) 4. [OAuth redirect flow](#oauth-redirect-flow) 5. [Supported banks](#supported-banks) 6. [Troubleshooting](#troubleshooting) 7. [Data & privacy](#data--privacy) 8. [Disclaimer](#disclaimer) 9. [License](#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](https://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. ```bash # 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 ```bash 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: ```yaml 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 edit `config.proxy_base_url` directly in `data/store.json` and restart the container. ### 5. Start the services ```bash docker compose up -d ``` Verify the proxy is up: ```bash 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. ```yaml # 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: ```bash # Create vault file ansible-vault create group_vars/actual_host/vault.yml ``` ```yaml # 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: ```bash ansible-playbook deploy.yml --ask-vault-pass ``` > **First deploy only:** `store.json` is written with `creates:` 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 run `npm run setup` manually. --- ## 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: } ← { url: "https://tilisy.enablebanking.com/..." } ↓ 3. proxy returns requisition { id, status: "CR", link: } ↓ 4. User visits the link, authenticates at their bank ↓ 5. Enable Banking → http://proxy/callback?code=xxx&state= ↓ 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= ↓ 7. actual-server GET /api/v2/requisitions// ← { status: "LN", accounts: ["gc-uuid-1", ...] } ↓ 8. actual-server GET /api/v2/accounts//details/ GET /api/v2/accounts//balances/ GET /api/v2/accounts//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/](https://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_url` in `data/store.json` must 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: ```bash 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.