Skip to main content

Advanced Integration Scenarios

Complex integration patterns and real-world use cases for production deployments.

This guide covers advanced scenarios beyond basic API integration.


1. Multi-Bank Account Aggregation​

Aggregate accounts from multiple banks for a single user.

Multi-Bank Flow​

Implementation Strategy​

  1. Create separate consents per bank
  2. Aggregate data from multiple sources
  3. Normalize different data structures
  4. Handle varying consent durations
  5. Merge transactions chronologically

Code Example​

class MultiBankAggregator {
constructor(apiClient) {
this.apiClient = apiClient;
this.banks = new Map();
}

async addBank(bankId, userId) {
// Create consent for specific bank
const consent = await this.apiClient.post('/v1/consents', {
access: {
allPsd2: 'allAccounts'
},
recurringIndicator: true,
validUntil: this.calculateValidUntil(90),
frequencyPerDay: 4
});

// Store consent mapping
this.banks.set(bankId, {
consentId: consent.consentId,
userId: userId,
bankId: bankId,
authUrl: consent._links.scaRedirect.href,
status: 'pending_authorization'
});

return {
bankId,
consentId: consent.consentId,
authUrl: consent._links.scaRedirect.href
};
}

async getAggregatedAccounts(userId) {
const allAccounts = [];

// Get consents for this user
const userConsents = Array.from(this.banks.values())
.filter(bank => bank.userId === userId && bank.status === 'valid');

// Fetch accounts from all banks in parallel
const accountPromises = userConsents.map(async (bank) => {
try {
const accounts = await this.apiClient.get('/v1/accounts', {
headers: { 'Consent-ID': bank.consentId }
});

// Add bank identifier to each account
return accounts.accounts.map(account => ({
...account,
bankId: bank.bankId,
bankName: this.getBankName(bank.bankId),
consentId: bank.consentId
}));
} catch (error) {
console.error(`Failed to fetch accounts from ${bank.bankId}:`, error);
return [];
}
});

const results = await Promise.all(accountPromises);

// Flatten and return
return results.flat();
}

async getAggregatedBalances(userId) {
const accounts = await this.getAggregatedAccounts(userId);

// Fetch balances for all accounts in parallel
const balancePromises = accounts.map(async (account) => {
try {
const balances = await this.apiClient.get(
`/v1/accounts/${account.resourceId}/balances`,
{
headers: { 'Consent-ID': account.consentId }
}
);

return {
accountId: account.resourceId,
iban: account.iban,
bankName: account.bankName,
balances: balances.balances
};
} catch (error) {
console.error(`Failed to fetch balance for ${account.iban}:`, error);
return null;
}
});

const results = await Promise.all(balancePromises);

return results.filter(result => result !== null);
}

async getAggregatedTransactions(userId, dateFrom, dateTo) {
const accounts = await this.getAggregatedAccounts(userId);

// Fetch transactions from all accounts
const transactionPromises = accounts.map(async (account) => {
try {
const transactions = await this.apiClient.get(
`/v1/accounts/${account.resourceId}/transactions`,
{
headers: { 'Consent-ID': account.consentId },
params: { dateFrom, dateTo }
}
);

// Add account context to each transaction
return transactions.transactions.booked.map(tx => ({
...tx,
accountId: account.resourceId,
iban: account.iban,
bankName: account.bankName
}));
} catch (error) {
console.error(`Failed to fetch transactions for ${account.iban}:`, error);
return [];
}
});

const results = await Promise.all(transactionPromises);

// Merge and sort by date
const allTransactions = results.flat();
allTransactions.sort((a, b) =>
new Date(b.bookingDate) - new Date(a.bookingDate)
);

return allTransactions;
}

calculateValidUntil(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString().split('T')[0];
}

getBankName(bankId) {
const bankNames = {
'swedbank': 'Swedbank',
'seb': 'SEB',
'luminor': 'Luminor',
'revolut': 'Revolut'
};
return bankNames[bankId] || bankId;
}
}

// Usage
const aggregator = new MultiBankAggregator(apiClient);

// User adds Swedbank
const swedbank = await aggregator.addBank('swedbank', 'user-123');
console.log('Authorize Swedbank:', swedbank.authUrl);

// User adds SEB
const seb = await aggregator.addBank('seb', 'user-123');
console.log('Authorize SEB:', seb.authUrl);

// After authorization, get aggregated data
const accounts = await aggregator.getAggregatedAccounts('user-123');
const balances = await aggregator.getAggregatedBalances('user-123');
const transactions = await aggregator.getAggregatedTransactions(
'user-123',
'2026-01-01',
'2026-02-10'
);

console.log(`Total accounts: ${accounts.length}`);
console.log(`Total balances: ${balances.length}`);
console.log(`Total transactions: ${transactions.length}`);

Error Handling​

Handle partial failures gracefully:

async function getAggregatedDataWithFallback(userId) {
const results = {
accounts: [],
errors: []
};

const consents = getValidConsents(userId);

for (const consent of consents) {
try {
const accounts = await fetchAccounts(consent.consentId);
results.accounts.push(...accounts);
} catch (error) {
// Continue with other banks even if one fails
results.errors.push({
bankId: consent.bankId,
error: error.message
});
console.error(`Failed to fetch from ${consent.bankId}:`, error);
}
}

return results;
}

Proactively renew consents before expiration.

Strategy​

  1. Track consent expiration dates
  2. Trigger renewal 7 days before expiry
  3. Notify users proactively
  4. Implement background renewal job

Implementation​

class ConsentRenewalManager {
constructor(apiClient, db) {
this.apiClient = apiClient;
this.db = db;
}

async checkExpiringConsents() {
const sevenDaysFromNow = new Date();
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);

// Find consents expiring soon
const expiringConsents = await this.db.query(`
SELECT * FROM consents
WHERE valid_until <= $1
AND status = 'valid'
AND renewal_notified = FALSE
`, [sevenDaysFromNow]);

for (const consent of expiringConsents) {
await this.triggerRenewal(consent);
}
}

async triggerRenewal(oldConsent) {
try {
// Create new consent with same scope
const newConsent = await this.apiClient.post('/v1/consents', {
access: oldConsent.access,
recurringIndicator: true,
validUntil: this.calculateValidUntil(90),
frequencyPerDay: 4
});

// Notify user
await this.notifyUser(oldConsent.userId, {
type: 'consent_renewal_required',
oldConsentId: oldConsent.id,
newConsentId: newConsent.consentId,
authUrl: newConsent._links.scaRedirect.href,
expiresAt: oldConsent.valid_until
});

// Mark as notified
await this.db.query(`
UPDATE consents
SET renewal_notified = TRUE,
renewal_consent_id = $1
WHERE id = $2
`, [newConsent.consentId, oldConsent.id]);

console.log(`Renewal triggered for consent ${oldConsent.id}`);

} catch (error) {
console.error(`Failed to renew consent ${oldConsent.id}:`, error);
}
}

async scheduleRenewalCheck() {
// Run daily at 9 AM
const schedule = require('node-schedule');

schedule.scheduleJob('0 9 * * *', async () => {
console.log('Running consent renewal check...');
await this.checkExpiringConsents();
});
}

calculateValidUntil(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString().split('T')[0];
}

async notifyUser(userId, notification) {
// Send email, push notification, in-app message
await this.sendEmail(userId, {
subject: 'Please renew your bank connection',
template: 'consent_renewal',
data: notification
});
}
}

// Usage
const renewalManager = new ConsentRenewalManager(apiClient, db);

// Start background job
renewalManager.scheduleRenewalCheck();

// Manual check
await renewalManager.checkExpiringConsents();

3. Payment Status Polling (Without Webhooks)​

Efficiently poll payment status when webhooks aren't available.

Smart Polling Flow​

Smart Polling Strategy​

Polling Pattern Recommendation

This is a suggested polling pattern that balances responsiveness with API efficiency. Adjust intervals based on your use case and rate limit constraints (4 req/day for AIS without user presence).

Suggested Intervals:

  • Initial period: Frequent checks (e.g., every few seconds)
  • Middle period: Reduced frequency (e.g., every 30 seconds)
  • Extended period: Infrequent checks (e.g., every few minutes)
  • Stop after: Payment reaches final status or timeout (e.g., 24 hours)

Implementation​

class PaymentStatusPoller {
constructor(apiClient) {
this.apiClient = apiClient;
this.pollers = new Map();
}

async startPolling(paymentId, onStatusChange) {
// Prevent duplicate pollers
if (this.pollers.has(paymentId)) {
console.log(`Already polling ${paymentId}`);
return;
}

const poller = {
paymentId,
startTime: Date.now(),
lastStatus: null,
attempts: 0,
onStatusChange
};

this.pollers.set(paymentId, poller);

await this.poll(paymentId);
}

async poll(paymentId) {
const poller = this.pollers.get(paymentId);
if (!poller) return;

try {
// Fetch current status
const payment = await this.apiClient.get(`/v1/payments/${paymentId}`);
const currentStatus = payment.transactionStatus;

poller.attempts++;

// Check if status changed
if (currentStatus !== poller.lastStatus) {
poller.lastStatus = currentStatus;
await poller.onStatusChange(payment);
}

// Stop polling if terminal state reached
if (this.isTerminalState(currentStatus)) {
console.log(`Payment ${paymentId} reached terminal state: ${currentStatus}`);
this.stopPolling(paymentId);
return;
}

// Stop after 24 hours
const elapsed = Date.now() - poller.startTime;
if (elapsed > 24 * 60 * 60 * 1000) {
console.log(`Payment ${paymentId} polling timeout (24h)`);
this.stopPolling(paymentId);
return;
}

// Calculate next poll interval
const delay = this.calculateDelay(elapsed);

// Schedule next poll
setTimeout(() => this.poll(paymentId), delay);

} catch (error) {
console.error(`Error polling payment ${paymentId}:`, error);

// Retry with exponential backoff
const delay = Math.min(poller.attempts * 1000, 60000);
setTimeout(() => this.poll(paymentId), delay);
}
}

calculateDelay(elapsedMs) {
const minutes = elapsedMs / (60 * 1000);

if (minutes < 1) {
return 5000; // 5 seconds for first minute
} else if (minutes < 5) {
return 30000; // 30 seconds for next 5 minutes
} else {
return 300000; // 5 minutes after that
}
}

isTerminalState(status) {
return ['ACSC', 'RJCT', 'CANC'].includes(status);
}

stopPolling(paymentId) {
this.pollers.delete(paymentId);
}
}

// Usage
const poller = new PaymentStatusPoller(apiClient);

// Start polling for payment
await poller.startPolling('pmt-123', async (payment) => {
console.log(`Payment status changed: ${payment.transactionStatus}`);

// Update database
await db.query(
'UPDATE payments SET status = $1 WHERE payment_id = $2',
[payment.transactionStatus, payment.paymentId]
);

// Notify user
if (payment.transactionStatus === 'ACSC') {
await notifyUser(payment.debtorAccount, 'Payment completed successfully');
} else if (payment.transactionStatus === 'RJCT') {
await notifyUser(payment.debtorAccount, 'Payment was rejected');
}
});

4. Multi-Currency Account Management​

Handle accounts in different currencies.

Considerations​

  • Exchange rate handling
  • Currency conversion
  • Display formatting
  • Transaction categorization per currency

Implementation​

class MultiCurrencyManager {
constructor(apiClient) {
this.apiClient = apiClient;
this.exchangeRates = new Map();
}

async getAggregatedBalance(accounts) {
const baseCurrency = 'EUR';
let totalInBaseCurrency = 0;

const balances = [];

for (const account of accounts) {
const accountBalance = await this.apiClient.get(
`/v1/accounts/${account.resourceId}/balances`
);

const balance = this.extractBalance(accountBalance.balances);

// Convert to base currency
const rate = await this.getExchangeRate(account.currency, baseCurrency);
const convertedAmount = balance.amount * rate;

totalInBaseCurrency += convertedAmount;

balances.push({
iban: account.iban,
currency: account.currency,
amount: balance.amount,
convertedAmount: convertedAmount,
baseCurrency: baseCurrency,
exchangeRate: rate
});
}

return {
totalInBaseCurrency,
baseCurrency,
balances
};
}

extractBalance(balances) {
// Prefer interimAvailable, fallback to closingBooked
const balance = balances.find(b => b.balanceType === 'interimAvailable') ||
balances.find(b => b.balanceType === 'closingBooked');

return {
amount: parseFloat(balance.balanceAmount.amount),
currency: balance.balanceAmount.currency
};
}

async getExchangeRate(fromCurrency, toCurrency) {
if (fromCurrency === toCurrency) return 1.0;

const cacheKey = `${fromCurrency}-${toCurrency}`;

// Check cache (1 hour TTL)
if (this.exchangeRates.has(cacheKey)) {
const cached = this.exchangeRates.get(cacheKey);
if (Date.now() - cached.timestamp < 3600000) {
return cached.rate;
}
}

// Fetch from exchange rate API
const rate = await this.fetchExchangeRate(fromCurrency, toCurrency);

this.exchangeRates.set(cacheKey, {
rate,
timestamp: Date.now()
});

return rate;
}

async fetchExchangeRate(from, to) {
// Use external API (e.g., ECB, fixer.io)
// Simplified example
const response = await fetch(
`https://api.exchangerate.host/latest?base=${from}&symbols=${to}`
);
const data = await response.json();
return data.rates[to];
}

formatCurrency(amount, currency) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
}).format(amount);
}
}

// Usage
const currencyManager = new MultiCurrencyManager(apiClient);

const accounts = [
{ resourceId: 'acc-1', iban: 'LT...', currency: 'EUR' },
{ resourceId: 'acc-2', iban: 'LT...', currency: 'USD' },
{ resourceId: 'acc-3', iban: 'LT...', currency: 'GBP' }
];

const aggregated = await currencyManager.getAggregatedBalance(accounts);

console.log(`Total (EUR): ${currencyManager.formatCurrency(aggregated.totalInBaseCurrency, 'EUR')}`);

aggregated.balances.forEach(balance => {
console.log(`${balance.iban}: ${currencyManager.formatCurrency(balance.amount, balance.currency)} = ${currencyManager.formatCurrency(balance.convertedAmount, 'EUR')} (rate: ${balance.exchangeRate})`);
});

5. Bulk Payment Processing​

Process multiple payments efficiently.

Queue-Based Implementation​

const Queue = require('bull');

class BulkPaymentProcessor {
constructor(apiClient) {
this.apiClient = apiClient;
this.paymentQueue = new Queue('payments', {
redis: { port: 6379, host: '127.0.0.1' }
});

this.setupProcessor();
}

setupProcessor() {
// Process payments one at a time (rate limiting)
this.paymentQueue.process(1, async (job) => {
const { payment, userId } = job.data;

try {
// Initiate payment
const result = await this.apiClient.post('/v1/payments', payment);

// Update progress
await job.progress(100);

return {
success: true,
paymentId: result.paymentId,
status: result.transactionStatus
};

} catch (error) {
// Handle payment failure
console.error(`Payment failed:`, error);

throw error; // Will trigger retry
}
});

// Listen for completion
this.paymentQueue.on('completed', (job, result) => {
console.log(`Payment ${result.paymentId} completed`);
});

this.paymentQueue.on('failed', (job, error) => {
console.error(`Payment failed:`, error);
});
}

async addBulkPayments(payments, userId) {
const jobs = [];

for (const payment of payments) {
const job = await this.paymentQueue.add(
{ payment, userId },
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: false
}
);

jobs.push(job);
}

return jobs;
}

async getBulkStatus(jobIds) {
const statuses = [];

for (const jobId of jobIds) {
const job = await this.paymentQueue.getJob(jobId);

statuses.push({
jobId,
state: await job.getState(),
progress: job.progress(),
result: job.returnvalue,
failedReason: job.failedReason
});
}

return statuses;
}
}

// Usage
const processor = new BulkPaymentProcessor(apiClient);

// Add 100 payments
const payments = [
{
instructedAmount: { amount: '100.00', currency: 'EUR' },
creditorAccount: { iban: 'LT...' },
creditorName: 'Employee 1'
},
// ... 99 more
];

const jobs = await processor.addBulkPayments(payments, 'user-123');

console.log(`Queued ${jobs.length} payments`);

// Check status
const statuses = await processor.getBulkStatus(jobs.map(j => j.id));
console.log(`Completed: ${statuses.filter(s => s.state === 'completed').length}`);

Track complete consent lifecycle.

State Machine​

Implementation​

class ConsentLifecycleManager {
constructor(apiClient, db) {
this.apiClient = apiClient;
this.db = db;
}

async createConsent(userId, access) {
// Create consent
const consent = await this.apiClient.post('/v1/consents', {
access,
recurringIndicator: true,
validUntil: this.calculateValidUntil(90),
frequencyPerDay: 4
});

// Store in database
await this.db.query(`
INSERT INTO consents (
consent_id, user_id, status, access,
valid_until, created_at
) VALUES ($1, $2, $3, $4, $5, NOW())
`, [
consent.consentId,
userId,
'pending_authorization',
JSON.stringify(access),
consent.validUntil
]);

return consent;
}

async handleAuthorization(consentId) {
// Update status after user authorizes
await this.db.query(`
UPDATE consents
SET status = 'valid',
authorized_at = NOW()
WHERE consent_id = $1
`, [consentId]);

console.log(`Consent ${consentId} authorized`);
}

async handleExpiration(consentId) {
// Consent expired
await this.db.query(`
UPDATE consents
SET status = 'expired',
expired_at = NOW()
WHERE consent_id = $1
`, [consentId]);

// Trigger renewal
const consent = await this.getConsent(consentId);
await this.triggerRenewal(consent);
}

async handleRevocation(consentId) {
// User revoked consent
await this.db.query(`
UPDATE consents
SET status = 'revoked',
revoked_at = NOW()
WHERE consent_id = $1
`, [consentId]);

// Stop all scheduled operations
await this.cancelScheduledOperations(consentId);
}

async monitorConsentHealth() {
// Check for expired consents
const expired = await this.db.query(`
SELECT * FROM consents
WHERE status = 'valid'
AND valid_until < NOW()
`);

for (const consent of expired) {
await this.handleExpiration(consent.consent_id);
}
}

calculateValidUntil(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString().split('T')[0];
}
}

7. Transaction Categorization & Analysis​

Categorize and analyze transactions.

Implementation​

class TransactionAnalyzer {
constructor() {
this.categories = {
'Groceries': ['maxima', 'rimi', 'iki', 'lidl'],
'Transport': ['bolt', 'wolt', 'uber', 'ryanair'],
'Utilities': ['telia', 'teo', 'eso', 'ignitis'],
'Entertainment': ['netflix', 'spotify', 'steam'],
'Restaurants': ['mcdonalds', 'kfc', 'pizza']
};
}

categorize(transactions) {
return transactions.map(tx => ({
...tx,
category: this.detectCategory(tx)
}));
}

detectCategory(transaction) {
const merchant = (
transaction.creditorName ||
transaction.remittanceInformationUnstructured ||
''
).toLowerCase();

for (const [category, keywords] of Object.entries(this.categories)) {
if (keywords.some(keyword => merchant.includes(keyword))) {
return category;
}
}

return 'Other';
}

analyze(transactions) {
const categorized = this.categorize(transactions);

// Group by category
const byCategory = {};

for (const tx of categorized) {
const amount = parseFloat(tx.transactionAmount.amount);

if (!byCategory[tx.category]) {
byCategory[tx.category] = {
count: 0,
total: 0,
transactions: []
};
}

byCategory[tx.category].count++;
byCategory[tx.category].total += Math.abs(amount);
byCategory[tx.category].transactions.push(tx);
}

return byCategory;
}
}

// Usage
const analyzer = new TransactionAnalyzer();

const transactions = await apiClient.get('/v1/accounts/123/transactions');
const analysis = analyzer.analyze(transactions.transactions.booked);

console.log('Spending by category:');
for (const [category, data] of Object.entries(analysis)) {
console.log(`${category}: €${data.total.toFixed(2)} (${data.count} transactions)`);
}

8. SCA (Strong Customer Authentication) Handling​

Handle different SCA flows.

Redirect Flow​

async function handleRedirectFlow(paymentData) {
// 1. Initiate payment
const payment = await apiClient.post('/v1/payments', paymentData);

// 2. Redirect user to bank for SCA
const scaUrl = payment._links.scaRedirect.href;
window.location.href = scaUrl;

// 3. User completes SCA at bank
// 4. Bank redirects back to your redirect_url
// 5. Check payment status
}

// On redirect callback
app.get('/payment-callback', async (req, res) => {
const paymentId = req.query.paymentId;

// Check status
const payment = await apiClient.get(`/v1/payments/${paymentId}`);

if (payment.transactionStatus === 'ACSC') {
res.send('Payment successful!');
} else {
res.send('Payment failed');
}
});


Support​

Need help with complex integrations?

Contact: tech_support@paysera.com