PSD2 (the Payment Services Directive 2) changed how European fintech apps handle payments and account access. It introduced Strong Customer Authentication (SCA), mandated open banking APIs, and created a regulatory framework for third-party providers (TPPs). Deep links are central to PSD2 flows because they handle the redirects between banking apps, third-party apps, and authentication screens.
This guide covers how deep links interact with PSD2 requirements. For fintech compliance broadly, see fintech compliance and deep links. For fintech security, see security best practices for fintech deep links.
PSD2 and Deep Links
PSD2 creates three scenarios where deep links are essential:
- SCA redirects. When a payment requires Strong Customer Authentication, the user must be redirected to their bank's app to authenticate, then redirected back.
- Account access consent. When a TPP (like a budgeting app) requests access to a user's bank account, the user is redirected to the bank's app to grant consent.
- Payment initiation. When a TPP initiates a payment, the user is redirected to their bank's app to authorize it.
Each of these flows involves at least two app-to-app redirects, and deep links are how those redirects work on mobile.
Strong Customer Authentication (SCA) Redirects
SCA requires two of three authentication factors: something the user knows (password/PIN), something they have (phone/card), and something they are (biometric). In mobile apps, this often means redirecting to the bank's app for biometric authentication.
SCA Redirect Flow
1. User initiates payment in merchant app
2. Merchant app calls PSP (Payment Service Provider) API
3. PSP returns SCA redirect URL → bank app deep link
4. Merchant app opens bank app via deep link
5. User authenticates in bank app (biometric + PIN)
6. Bank app redirects back to merchant app via callback deep link
7. Merchant app receives authorization and completes payment
Implementation
// Step 4: Open bank app for SCA
func initiatePaymentWithSCA(paymentIntent: PaymentIntent) {
guard let scaRedirectURL = paymentIntent.scaRedirectURL else {
// SCA not required, complete payment directly
completePayment(paymentIntent)
return
}
// Store payment context for when the user returns
pendingPayment = paymentIntent
// Open the bank's app via Universal Link
UIApplication.shared.open(scaRedirectURL, options: [:]) { opened in
if !opened {
// Bank app not installed, fall back to web-based SCA
self.showWebSCA(url: scaRedirectURL)
}
}
}
// Step 7: Handle callback from bank app
func handleSCACallback(_ url: URL) {
let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
let status = params?.first(where: { $0.name == "status" })?.value
let authCode = params?.first(where: { $0.name == "auth_code" })?.value
guard let payment = pendingPayment else {
showError("No pending payment found.")
return
}
if status == "authenticated", let authCode = authCode {
completePayment(payment, authCode: authCode)
} else {
showPaymentFailed(reason: "Authentication was declined or cancelled.")
}
pendingPayment = nil
}
SCA Callback Deep Link
The callback URL must be registered as a Universal Link (iOS) or App Link (Android):
https://links.merchantapp.com/payments/sca/callback?status=authenticated&auth_code=ABC123
Register the route:
/payments/sca/callback → Handle SCA authentication result
Account Access Consent (AIS)
Under PSD2, Account Information Service Providers (AISPs) can access bank account data with the user's consent. The consent flow uses deep links to redirect between the AISP's app and the bank's app.
Consent Flow
1. User requests to connect bank account in budgeting app (AISP)
2. AISP app opens bank app via deep link with consent request
3. Bank app shows consent screen (which accounts, what data, how long)
4. User approves consent in bank app
5. Bank app redirects back to AISP app with authorization code
6. AISP app exchanges code for access token
AISP Side (Budgeting App)
func connectBankAccount(bank: Bank) {
let consentURL = bank.buildConsentURL(
clientId: "AISP-CLIENT-ID",
scope: "accounts,balances,transactions",
redirectURI: "https://links.budgetapp.com/connect/bank/callback",
state: generateState()
)
UIApplication.shared.open(consentURL)
}
func handleBankCallback(_ url: URL) {
let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
let code = params?.first(where: { $0.name == "code" })?.value
let state = params?.first(where: { $0.name == "state" })?.value
guard validateState(state) else {
showError("Invalid session. Please try again.")
return
}
if let code = code {
exchangeCodeForToken(code)
}
}
Bank Side
The bank app receives the consent request deep link and shows the consent screen:
func handleConsentRequest(_ url: URL) {
guard authManager.isAuthenticated else {
pendingDeepLink = url
showBiometricLogin()
return
}
let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
let clientId = params?.first(where: { $0.name == "client_id" })?.value
let scope = params?.first(where: { $0.name == "scope" })?.value
let redirectURI = params?.first(where: { $0.name == "redirect_uri" })?.value
guard let tpp = registeredTPPs[clientId] else {
showError("Unrecognized third party.")
return
}
showConsentScreen(
tppName: tpp.name,
requestedScope: scope,
redirectURI: redirectURI
)
}
Payment Initiation (PIS)
Payment Initiation Service Providers (PISPs) can initiate payments from a user's bank account. The flow is similar to AIS consent but includes payment details.
Payment Initiation Deep Link
https://links.bankapp.com/psd2/payment?
pisp=PISP-CLIENT-ID&
amount=50.00&
currency=EUR&
recipient=Merchant+Name&
iban=DE89370400440532013000&
reference=ORDER-789&
redirect_uri=https://links.merchantapp.com/payments/callback&
state=random_state_value
The bank app opens, shows the payment details, requires SCA, and redirects back to the PISP's app after authorization.
Handling Fallbacks
Not all users have their bank's app installed. PSD2 flows must handle this gracefully:
func openBankForSCA(scaURL: URL, bank: Bank) {
UIApplication.shared.open(scaURL, options: [:]) { opened in
if !opened {
// Option 1: Open bank's mobile website
if let webURL = bank.webAuthURL {
let safariVC = SFSafariViewController(url: webURL)
self.present(safariVC, animated: true)
}
// Option 2: Show "install bank app" message
else {
self.showBankAppRequired(bank: bank)
}
}
}
}
On Android, use App Links verification to ensure the bank's app handles the intent:
fun openBankForSCA(scaUrl: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(scaUrl))
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
// Fall back to web-based authentication
val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(scaUrl))
webIntent.setPackage("com.android.chrome")
startActivity(webIntent)
}
}
Security Requirements
State Parameter
Every PSD2 redirect must include a state parameter to prevent CSRF attacks. Generate a random value, store it, and verify it when the user returns:
func generateState() -> String {
let state = UUID().uuidString
UserDefaults.standard.set(state, forKey: "psd2_state")
return state
}
func validateState(_ state: String?) -> Bool {
let stored = UserDefaults.standard.string(forKey: "psd2_state")
UserDefaults.standard.removeObject(forKey: "psd2_state")
return state != nil && state == stored
}
Redirect URI Validation
Banks must validate that the redirect URI matches the registered TPP. Do not accept arbitrary redirect URIs from deep links.
Certificate Validation
Under PSD2, TPPs must use eIDAS certificates for API communication. While this is server-side, it affects the deep link flow because the bank must verify the TPP's identity before showing the consent screen.
Regional Considerations
PSD2 applies to the European Economic Area (EEA). Similar regulations exist in other regions:
| Region | Regulation | Deep Link Impact |
|---|---|---|
| EU/EEA | PSD2 | SCA redirects, TPP consent flows |
| UK | UK Open Banking | Similar to PSD2, uses UK-specific standards |
| Australia | CDR (Consumer Data Right) | Data sharing consent flows |
| Brazil | Open Finance Brazil | Payment initiation and consent |
| India | Account Aggregator | Consent-based data sharing |
Each regulation has its own redirect flow patterns, but the deep linking principles are the same: redirect to the bank app for consent/authentication, then redirect back with the result.
Tolinku for PSD2 Flows
Tolinku supports the route patterns PSD2 redirect flows need. Define callback routes in the Tolinku dashboard. Universal Links and App Links ensure that PSD2 redirects open the correct app reliably on both iOS and Android.
For fintech compliance, see fintech compliance and deep links. For fintech deep linking broadly, see deep linking for fintech and banking apps.
Get deep linking tips in your inbox
One email per week. No spam.