PSD2 & Open Banking: Enriching European Bank Transaction Data
PSD2 opened the door to European bank transaction data, but the raw feeds are notoriously messy. Here is how transaction enrichment turns cryptic European bank strings into structured, usable merchant information.
The revised Payment Services Directive (PSD2) fundamentally changed how fintech companies access bank data across Europe. Since its enforcement in 2019, any licensed third-party provider can request access to a customer's payment account data with their consent. This has created an enormous opportunity for fintech builders -- but it has also surfaced a difficult problem: the raw transaction data that European banks expose through their APIs is, in many cases, worse than what US banks provide.
PSD2: What It Means for Transaction Data Access
PSD2 requires banks in the European Economic Area (EEA) to provide Account Information Service Providers (AISPs) with access to customer account data through dedicated APIs. In practice, this means any authorized fintech application can pull a user's transaction history from their bank -- across all 27 EU member states, plus the UK (under its own Open Banking framework), Norway, Iceland, and Liechtenstein.
The regulation standardizes the right of access, but it does not standardize the format of that data. Each bank implements its own API (or uses a framework like the Berlin Group's NextGenPSD2 or the UK's Open Banking Standard), and each one returns transaction descriptions in its own style. This creates a fragmentation problem that is unique to Europe and significantly harder to solve than the equivalent issue in the US market.
The Data Quality Problem: Why PSD2 Feeds Are Messier Than US Bank Data
If you have worked with US bank transaction data through Plaid or MX, you know the descriptions are already rough -- strings like SQ *COFFEE SHOP NYC or AMZN MKTP US*M12345. European PSD2 feeds are worse for several structural reasons:
- Multiple languages: A single user's transaction history might contain descriptions in German, French, Dutch, and English. A Deutsche Bank feed might show "KARTENZAHLUNG REWE SAGT DANKE" while a BNP Paribas feed shows "PAIEMENT CB CARREFOUR".
- SEPA transfer noise: European transfers via SEPA include structured reference numbers, IBAN fragments, and remittance information embedded in the description field.
- Inconsistent encoding: Some banks strip diacritics (turning "Muller" into "MUELLER"), while others preserve them. Some truncate at 30 characters, others at 140.
- Country-specific card processor formats: Visa and Mastercard transactions surface differently depending on whether the acquiring bank is in Germany, Spain, or the Netherlands.
- No MCC codes in the feed: Unlike some US bank APIs that pass through merchant category codes, most European PSD2 APIs do not expose MCC data at all.
Here is what a typical batch of raw PSD2 transactions looks like:
// Raw PSD2 transactions from various European banks
[
{
"transactionId": "eu_txn_001",
"bookingDate": "2025-02-28",
"amount": { "amount": "-47.90", "currency": "EUR" },
"remittanceInformationUnstructured":
"KARTENZAHLUNG REWE SAGT DANKE 0129//BERLIN/DE 2025-02-28T14:23"
},
{
"transactionId": "eu_txn_002",
"bookingDate": "2025-02-27",
"amount": { "amount": "-12.50", "currency": "EUR" },
"remittanceInformationUnstructured":
"PAIEMENT CB 2702 BOULANGERIE PAUL PARIS 17EME"
},
{
"transactionId": "eu_txn_003",
"bookingDate": "2025-02-27",
"amount": { "amount": "-89.00", "currency": "SEK" },
"remittanceInformationUnstructured":
"KORKORT IKEA MALMO 582941****1234 25.02.27"
},
{
"transactionId": "eu_txn_004",
"bookingDate": "2025-02-26",
"amount": { "amount": "-250.00", "currency": "EUR" },
"remittanceInformationUnstructured":
"SEPA DD VATTENFALL EUROPE SALES REF MND-2025-882716 CREDITOR ID DE98ZZZ09999999999"
}
]Without enrichment, these descriptions are nearly unusable for end-user-facing applications. No human wants to read "KARTENZAHLUNG REWE SAGT DANKE 0129//BERLIN/DE" in their banking app.
European vs. US Transaction Formats
Understanding the structural differences helps explain why a one-size-fits-all enrichment approach does not work:
| Attribute | US (ACH / Plaid) | Europe (PSD2 / SEPA) |
|---|---|---|
| Description language | English | 24+ languages |
| Currency | USD (single) | EUR, GBP, SEK, CHF, PLN, etc. |
| Description field | name (short) | remittanceInformationUnstructured (long, noisy) |
| MCC code availability | Sometimes included | Rarely included |
| SEPA / transfer metadata | N/A | Creditor IDs, mandate refs, IBANs |
| API standard | Proprietary (Plaid, MX) | NextGenPSD2, UK OB, STET, PolishAPI |
The combination of multilingual descriptions, multiple currencies, and fragmented API standards makes European transaction enrichment a significantly harder problem than US-only enrichment.
How Enrichment Solves PSD2 Data Quality Issues
A transaction enrichment API takes the raw description string and returns structured data: a clean merchant name, a category, a logo URL, and often a website and location. For PSD2 data specifically, good enrichment needs to handle several additional challenges:
- Language detection: The enrichment engine must recognize that "KARTENZAHLUNG" is a German card payment prefix and strip it before identifying the merchant as REWE.
- SEPA metadata extraction: Direct debits contain creditor IDs, mandate references, and other structured data mixed into the free-text description. The API must parse these out.
- Cross-border merchant matching: A charge at "IKEA" should resolve to the same merchant whether the description is in Swedish, German, or French.
- Currency-aware categorization: A 500 SEK charge and a 500 EUR charge at the same merchant represent very different spending levels. Enrichment should factor this in.
Working with European Aggregators + Easy Enrichment
Most European fintechs access PSD2 data through aggregators rather than connecting directly to bank APIs. The three major European aggregators -- Tink (now part of Visa), TrueLayer, and Yapily -- each provide transaction data in slightly different formats, but all share the same core problem: the raw descriptions need enrichment.
Easy Enrichment works as a post-processing layer on top of any aggregator. The integration pattern is the same regardless of which provider you use:
User grants consent (PSD2 SCA flow)
|
Aggregator (Tink / TrueLayer / Yapily)
|
Raw PSD2 transactions
|
Easy Enrichment API <-- enrich here
|
Enriched: merchant name, category, logo, website
|
Your app database
|
User sees clean transaction feedCode Example: Enriching a Batch of PSD2 Transactions
Here is a complete example of fetching PSD2 transactions from Tink and enriching them in batch with Easy Enrichment. This pattern works with any aggregator -- swap the fetch call for TrueLayer or Yapily as needed.
import axios from 'axios';
const TINK_TOKEN = process.env.TINK_ACCESS_TOKEN;
const EE_API_KEY = process.env.EASY_ENRICHMENT_API_KEY;
// Step 1: Fetch transactions from Tink
async function fetchTinkTransactions(accountId: string) {
const response = await axios.get(
`https://api.tink.com/data/v2/accounts/${accountId}/transactions`,
{ headers: { Authorization: `Bearer ${TINK_TOKEN}` } }
);
return response.data.transactions;
}
// Step 2: Enrich in batch via Easy Enrichment
async function enrichBatch(transactions: any[]) {
const payload = transactions.map((txn) => ({
description:
txn.descriptions?.original ||
txn.remittanceInformationUnstructured ||
'',
amount: Math.abs(parseFloat(txn.amount.value)),
currency: txn.amount.currencyCode,
}));
const response = await axios.post(
'https://api.easyenrichment.com/enrich/batch',
{ transactions: payload },
{
headers: {
Authorization: `Bearer ${EE_API_KEY}`,
'Content-Type': 'application/json',
},
}
);
return response.data.results;
}
// Step 3: Merge enrichment data back into transactions
async function processAccount(accountId: string) {
const rawTxns = await fetchTinkTransactions(accountId);
const enrichments = await enrichBatch(rawTxns);
return rawTxns.map((txn: any, i: number) => ({
id: txn.transactionId,
date: txn.bookingDate,
amount: txn.amount,
raw_description: txn.descriptions?.original || '',
merchant_name: enrichments[i].merchant_name,
category: enrichments[i].category,
logo_url: enrichments[i].logo_url,
website: enrichments[i].website,
confidence: enrichments[i].confidence,
}));
}
// Usage
const enrichedTxns = await processAccount('acct_eu_12345');
console.log(enrichedTxns[0]);
// {
// id: "eu_txn_001",
// date: "2025-02-28",
// amount: { value: "-47.90", currencyCode: "EUR" },
// raw_description: "KARTENZAHLUNG REWE SAGT DANKE 0129//BERLIN/DE",
// merchant_name: "REWE",
// category: "Groceries",
// logo_url: "https://logos.easyenrichment.com/rewe.png",
// website: "rewe.de",
// confidence: 0.97
// }GDPR Considerations When Processing Transaction Data
Processing PSD2 transaction data in the EU means GDPR applies. Transaction descriptions can contain personally identifiable information -- names in SEPA transfers, addresses in card payment descriptions, and account references. Here is what you need to account for:
- Legal basis: You need a lawful basis for processing transaction data. If the user consented to PSD2 access, that covers retrieval, but enrichment via a third-party API requires either legitimate interest or an extension of the original consent.
- Data Processing Agreements: If you send transaction data to an enrichment API, that provider is a data processor under GDPR. You must have a DPA in place. Easy Enrichment provides a standard DPA on request.
- Data minimization: Only send the fields needed for enrichment (description, amount, currency). Do not send account holder names, IBANs, or balances to the enrichment endpoint.
- Storage limitations: Your enrichment provider should not retain raw transaction data beyond the time needed to process the request. Easy Enrichment processes requests in real-time and does not persist raw input data.
- Cross-border transfers: If your enrichment provider processes data outside the EEA, ensure adequate safeguards are in place (Standard Contractual Clauses, adequacy decisions, etc.).
- Right to erasure: If a user exercises their right to be forgotten, you need to delete enriched transaction data as well as raw data. Design your data model so that enrichment results are linked to the user and can be purged.
Multi-Currency and International Merchant Handling
European users frequently transact in multiple currencies. A German user might have EUR charges from local shops, GBP charges from a London trip, and SEK charges from an IKEA purchase in Sweden. Enrichment must handle this cleanly:
- Currency passthrough: Always pass the original transaction currency to the enrichment API. This helps disambiguate merchants that operate under different names in different markets.
- Cross-border merchant resolution: "AMZN MKTP DE" and "AMZN MKTP FR" should both resolve to Amazon, but with locale-specific metadata when available. An enrichment API should normalize the merchant identity while preserving regional context.
- Exchange rate annotations: Some bank descriptions include exchange rate information (e.g., "RATE 1.0845 EUR/GBP"). The enrichment layer should strip this noise from the description before matching.
- Local merchant databases: Effective European enrichment requires merchant databases that cover local chains -- not just global brands. REWE, Albert Heijn, Monoprix, and Mercadona are household names in their respective markets but invisible to US-focused enrichment engines.
Easy Enrichment's AI-powered matching handles multi-language descriptions and regional merchant variants out of the box. It processes descriptions in 30+ languages and maintains merchant reference data across all EEA markets.
Start Enriching PSD2 Transaction Data
Turn raw European bank feeds into clean, categorized transactions with merchant logos and websites. Works with Tink, TrueLayer, Yapily, and any PSD2-compliant data source.