devto 2026-07-22 원문 보기 ↗
If you've ever tried to call a European bank's open banking endpoint directly — say, to read your own account balances programmatically — you probably hit a wall that looks something like this:
HTTP 401 Unauthorized
{"error": "invalid_client", "error_description": "mutual TLS certificate required"}
That's the eIDAS certificate gate. Under PSD2, a bank's production API is protected by mutual TLS using a qualified certificate (a QWAC), and request signing often needs a second one (QSeal). For a regulated bank or a well-funded fintech, that's a tax you pay. For an indie developer, a student project, or a small business automating its own accounting, it's a deal-breaker: the certs cost roughly EUR 2,000–10,000 per year, take weeks of paperwork, and require a registered legal entity.
The good news: there's a well-defined, fully compliant path to read EU bank data without ever touching an eIDAS certificate yourself. This guide explains how it actually works under the hood, when to use it, and includes real, runnable code.
Disclosure up front: I'm John, the founder of open-banking.io — one of the providers in this space. I'll use our API for the worked code example because I know it best, but the architecture I'm describing is how every certificate-free aggregator works. I'll be honest about where direct access is the better choice.
Every PSD2 Account Information Service (AIS) call ultimately lands on the same bank endpoint. The question is who holds the certificate.
You apply for an eIDAS QWAC and QSeal from a qualified trust service provider, register as a TPP (Third Party Provider) with each national regulator, onboard with each bank's developer portal, and call the bank directly over mutual TLS.
You sign up with an AIS aggregator, get an API key, and call their unified API. They hold the eIDAS certificates, maintain the bank integrations, handle the per-bank quirks (Berlin Group vs STET vs UK Open Banking vs Polish API standards), and present you with one consistent REST interface.
| Dimension | Direct (eIDAS) | Aggregator (cert-free) |
|---|---|---|
| Certificate required? | QWAC + QSeal (you buy) | None |
| Setup time | Weeks–months | Minutes |
| Upfront cost | EUR 2k–10k/yr | $0 on free tiers |
| Bank coverage | Per-bank onboarding | 1,000s of banks, one API |
| Data path | You <-> Bank | You <-> Aggregator <-> Bank |
| GDPR control | Full (no intermediary) | Depends on provider (look for EU data residency + E2E encryption) |
| Per-bank quirks | You handle them all | Abstracted away |
The GDPR row is the one people underestimate. A good aggregator gives you EU-only data residency and ideally end-to-end encryption where only you hold the decryption key — meaning the aggregator genuinely can't read your transactions in the clear. That's the property you're trading the direct path for, so verify it before you sign up.
Aggregators don't "skip" PSD2 — they comply with it on your behalf. Here's the sequence, and it's the same whether you use Nordigen/GoCardless, Tink, TrueLayer, Enable Banking, or open-banking.io:
1. Create a consent / requisition -> you POST "user wants bank X"
2. Redirect user to bank (SCA) -> user authenticates at their bank
3. Bank calls back to aggregator -> consent is now "valid"
4. You list accounts -> GET /accounts
5. You fetch balances & transactions -> GET /accounts/{id}/transactions
6. Consent expires (90 / 180 days) -> repeat from step 1
The eIDAS certificate is used in step 2 and 3 — when the aggregator talks to the bank's production endpoints. Your API key only authenticates you to the aggregator, over standard HTTPS. You never see mTLS.
The two things that bite people:
Here's the actual pattern, using open-banking.io's API as the worked example. The shape (create requisition -> redirect -> list -> fetch) is identical across providers; only the field names change.
# Replace YOUR_API_KEY with the key from your provider's dashboard
curl -X POST https://api.open-banking.io/v1/requisitions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"redirect": "https://yourapp.example.com/callback",
"institution_id": "DANMARKS_DANSKE_BANK",
"reference": "user-42-first-link"
}'
{
"id": "req_8f3a...",
"link": "https://api.open-banking.io/redirect/req_8f3a...",
"accounts": [],
"status": "CR"
}
Send the user's browser to link. They authenticate at Danske Bank (SCA). On success, the bank redirects back to your redirect URL.
curl https://api.open-banking.io/v1/requisitions/req_8f3a... \
-H "Authorization: Bearer YOUR_API_KEY"
{
"id": "req_8f3a...",
"status": "LN",
"accounts": ["acc_a1b2...", "acc_c3d4..."]
}
status: "LN" means linked. Now grab data.
# Balances
curl https://api.open-banking.io/v1/accounts/acc_a1b2.../balances \
-H "Authorization: Bearer YOUR_API_KEY"
# Transactions (last 30 days)
curl "https://api.open-banking.io/v1/accounts/acc_a1b2.../transactions?date_from=2026-06-21&date_to=2026-07-21" \
-H "Authorization: Bearer YOUR_API_KEY"
import os, requests
API = "https://api.open-banking.io/v1"
KEY = os.environ["OBI_API_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
def create_requisition(institution_id, redirect_url):
r = requests.post(f"{API}/requisitions", headers=H, json={
"redirect": redirect_url,
"institution_id": institution_id,
})
r.raise_for_status()
return r.json()
def list_accounts(requisition_id):
r = requests.get(f"{API}/requisitions/{requisition_id}", headers=H)
return r.json()["accounts"]
def get_transactions(account_id, date_from, date_to):
r = requests.get(
f"{API}/accounts/{account_id}/transactions",
headers=H, params={"date_from": date_from, "date_to": date_to},
)
return r.json()["transactions"]
# Usage
req = create_requisition("DANMARKS_DANSKE_BANK", "https://yourapp.example.com/cb")
print("Send user to:", req["link"])
# ... after the user authenticates at their bank ...
for acct in list_accounts(req["id"]):
txns = get_transactions(acct, "2026-06-21", "2026-07-21")
print(acct, len(txns), "transactions")
Check each provider's live docs for the exact field names — they vary. The flow above is universal across PSD2 AIS.
booked and pending transactions. Dedupe by transactionId (or entryReference); if a bank doesn't supply one, hash (date, amount, counterparty, description) and treat collisions carefully.The aggregator path isn't always right. Go direct with your own eIDAS certificate when:
For everyone else — hobby projects, SMB accounting automation, personal finance dashboards, self-hosted budgeting tools (Actual Budget, Firefly III, Beancount integrations) — the certificate-free aggregator path is almost always the pragmatic choice. You get to ship this week instead of next quarter.
If you want to try the flow above end-to-end, you can grab a free API key at open-banking.io — and yes, that's my project, so apply appropriate skepticism and compare it against Tink, TrueLayer, GoCardless (formerly Nordigen), and Enable Banking before you commit. The right answer depends on your banks and your budget, not on who wrote this article.
Questions or war stories from your own bank-data integrations? Drop them in the comments — I read every one.