{"id":1707,"date":"2026-07-10T09:00:00","date_gmt":"2026-07-10T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1707"},"modified":"2026-03-07T03:50:02","modified_gmt":"2026-03-07T08:50:02","slug":"fintech-deep-linking-psd2","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/fintech-deep-linking-psd2\/","title":{"rendered":"Deep Linking and PSD2: Open Banking Considerations"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">PSD2 (the <a href=\"https:\/\/eur-lex.europa.eu\/legal-content\/EN\/TXT\/?uri=celex%3A32015L2366\" rel=\"nofollow noopener\" target=\"_blank\">Payment Services Directive 2<\/a>) 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers how deep links interact with PSD2 requirements. For fintech compliance broadly, see <a href=\"https:\/\/tolinku.com\/blog\/fintech-compliance-deep-links\/\">fintech compliance and deep links<\/a>. For fintech security, see <a href=\"https:\/\/tolinku.com\/blog\/fintech-deep-link-security\/\">security best practices for fintech deep links<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">PSD2 and Deep Links<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PSD2 creates three scenarios where deep links are essential:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>SCA redirects.<\/strong> When a payment requires Strong Customer Authentication, the user must be redirected to their bank&#39;s app to authenticate, then redirected back.<\/li>\n<li><strong>Account access consent.<\/strong> When a TPP (like a budgeting app) requests access to a user&#39;s bank account, the user is redirected to the bank&#39;s app to grant consent.<\/li>\n<li><strong>Payment initiation.<\/strong> When a TPP initiates a payment, the user is redirected to their bank&#39;s app to authorize it.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Each of these flows involves at least two app-to-app redirects, and deep links are how those redirects work on mobile.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Strong Customer Authentication (SCA) Redirects<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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&#39;s app for biometric authentication.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">SCA Redirect Flow<\/h3>\n\n\n\n<pre><code>1. User initiates payment in merchant app\n2. Merchant app calls PSP (Payment Service Provider) API\n3. PSP returns SCA redirect URL \u2192 bank app deep link\n4. Merchant app opens bank app via deep link\n5. User authenticates in bank app (biometric + PIN)\n6. Bank app redirects back to merchant app via callback deep link\n7. Merchant app receives authorization and completes payment\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Implementation<\/h3>\n\n\n\n<pre><code class=\"language-swift\">\/\/ Step 4: Open bank app for SCA\nfunc initiatePaymentWithSCA(paymentIntent: PaymentIntent) {\n    guard let scaRedirectURL = paymentIntent.scaRedirectURL else {\n        \/\/ SCA not required, complete payment directly\n        completePayment(paymentIntent)\n        return\n    }\n\n    \/\/ Store payment context for when the user returns\n    pendingPayment = paymentIntent\n\n    \/\/ Open the bank&#39;s app via Universal Link\n    UIApplication.shared.open(scaRedirectURL, options: [:]) { opened in\n        if !opened {\n            \/\/ Bank app not installed, fall back to web-based SCA\n            self.showWebSCA(url: scaRedirectURL)\n        }\n    }\n}\n\n\/\/ Step 7: Handle callback from bank app\nfunc handleSCACallback(_ url: URL) {\n    let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems\n    let status = params?.first(where: { $0.name == &quot;status&quot; })?.value\n    let authCode = params?.first(where: { $0.name == &quot;auth_code&quot; })?.value\n\n    guard let payment = pendingPayment else {\n        showError(&quot;No pending payment found.&quot;)\n        return\n    }\n\n    if status == &quot;authenticated&quot;, let authCode = authCode {\n        completePayment(payment, authCode: authCode)\n    } else {\n        showPaymentFailed(reason: &quot;Authentication was declined or cancelled.&quot;)\n    }\n\n    pendingPayment = nil\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">SCA Callback Deep Link<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The callback URL must be registered as a <a href=\"https:\/\/developer.apple.com\/documentation\/xcode\/allowing-apps-and-websites-to-link-to-your-content\/\" rel=\"nofollow noopener\" target=\"_blank\">Universal Link<\/a> (iOS) or <a href=\"https:\/\/developer.android.com\/training\/app-links\" rel=\"nofollow noopener\" target=\"_blank\">App Link<\/a> (Android):<\/p>\n\n\n\n<pre><code>https:\/\/links.merchantapp.com\/payments\/sca\/callback?status=authenticated&amp;auth_code=ABC123\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Register the route:<\/p>\n\n\n\n<pre><code>\/payments\/sca\/callback    \u2192 Handle SCA authentication result\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Account Access Consent (AIS)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Under PSD2, Account Information Service Providers (AISPs) can access bank account data with the user&#39;s consent. The consent flow uses deep links to redirect between the AISP&#39;s app and the bank&#39;s app.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Consent Flow<\/h3>\n\n\n\n<pre><code>1. User requests to connect bank account in budgeting app (AISP)\n2. AISP app opens bank app via deep link with consent request\n3. Bank app shows consent screen (which accounts, what data, how long)\n4. User approves consent in bank app\n5. Bank app redirects back to AISP app with authorization code\n6. AISP app exchanges code for access token\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">AISP Side (Budgeting App)<\/h3>\n\n\n\n<pre><code class=\"language-swift\">func connectBankAccount(bank: Bank) {\n    let consentURL = bank.buildConsentURL(\n        clientId: &quot;AISP-CLIENT-ID&quot;,\n        scope: &quot;accounts,balances,transactions&quot;,\n        redirectURI: &quot;https:\/\/links.budgetapp.com\/connect\/bank\/callback&quot;,\n        state: generateState()\n    )\n\n    UIApplication.shared.open(consentURL)\n}\n\nfunc handleBankCallback(_ url: URL) {\n    let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems\n    let code = params?.first(where: { $0.name == &quot;code&quot; })?.value\n    let state = params?.first(where: { $0.name == &quot;state&quot; })?.value\n\n    guard validateState(state) else {\n        showError(&quot;Invalid session. Please try again.&quot;)\n        return\n    }\n\n    if let code = code {\n        exchangeCodeForToken(code)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Bank Side<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The bank app receives the consent request deep link and shows the consent screen:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func handleConsentRequest(_ url: URL) {\n    guard authManager.isAuthenticated else {\n        pendingDeepLink = url\n        showBiometricLogin()\n        return\n    }\n\n    let params = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems\n    let clientId = params?.first(where: { $0.name == &quot;client_id&quot; })?.value\n    let scope = params?.first(where: { $0.name == &quot;scope&quot; })?.value\n    let redirectURI = params?.first(where: { $0.name == &quot;redirect_uri&quot; })?.value\n\n    guard let tpp = registeredTPPs[clientId] else {\n        showError(&quot;Unrecognized third party.&quot;)\n        return\n    }\n\n    showConsentScreen(\n        tppName: tpp.name,\n        requestedScope: scope,\n        redirectURI: redirectURI\n    )\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Payment Initiation (PIS)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Payment Initiation Service Providers (PISPs) can initiate payments from a user&#39;s bank account. The flow is similar to AIS consent but includes payment details.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Payment Initiation Deep Link<\/h3>\n\n\n\n<pre><code>https:\/\/links.bankapp.com\/psd2\/payment?\n  pisp=PISP-CLIENT-ID&amp;\n  amount=50.00&amp;\n  currency=EUR&amp;\n  recipient=Merchant+Name&amp;\n  iban=DE89370400440532013000&amp;\n  reference=ORDER-789&amp;\n  redirect_uri=https:\/\/links.merchantapp.com\/payments\/callback&amp;\n  state=random_state_value\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The bank app opens, shows the payment details, requires SCA, and redirects back to the PISP&#39;s app after authorization.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Fallbacks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Not all users have their bank&#39;s app installed. PSD2 flows must handle this gracefully:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func openBankForSCA(scaURL: URL, bank: Bank) {\n    UIApplication.shared.open(scaURL, options: [:]) { opened in\n        if !opened {\n            \/\/ Option 1: Open bank&#39;s mobile website\n            if let webURL = bank.webAuthURL {\n                let safariVC = SFSafariViewController(url: webURL)\n                self.present(safariVC, animated: true)\n            }\n            \/\/ Option 2: Show &quot;install bank app&quot; message\n            else {\n                self.showBankAppRequired(bank: bank)\n            }\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On Android, use <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks\" rel=\"nofollow noopener\" target=\"_blank\">App Links verification<\/a> to ensure the bank&#39;s app handles the intent:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">fun openBankForSCA(scaUrl: String) {\n    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(scaUrl))\n\n    try {\n        startActivity(intent)\n    } catch (e: ActivityNotFoundException) {\n        \/\/ Fall back to web-based authentication\n        val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(scaUrl))\n        webIntent.setPackage(&quot;com.android.chrome&quot;)\n        startActivity(webIntent)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Security Requirements<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">State Parameter<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every PSD2 redirect must include a <code>state<\/code> parameter to prevent CSRF attacks. Generate a random value, store it, and verify it when the user returns:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func generateState() -&gt; String {\n    let state = UUID().uuidString\n    UserDefaults.standard.set(state, forKey: &quot;psd2_state&quot;)\n    return state\n}\n\nfunc validateState(_ state: String?) -&gt; Bool {\n    let stored = UserDefaults.standard.string(forKey: &quot;psd2_state&quot;)\n    UserDefaults.standard.removeObject(forKey: &quot;psd2_state&quot;)\n    return state != nil &amp;&amp; state == stored\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Redirect URI Validation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Banks must validate that the redirect URI matches the registered TPP. Do not accept arbitrary redirect URIs from deep links.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Certificate Validation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Under PSD2, TPPs must use <a href=\"https:\/\/www.etsi.org\/technologies\/electronic-signatures-and-infrastructures-esi\" rel=\"nofollow noopener\" target=\"_blank\">eIDAS certificates<\/a> for API communication. While this is server-side, it affects the deep link flow because the bank must verify the TPP&#39;s identity before showing the consent screen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Regional Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PSD2 applies to the European Economic Area (EEA). Similar regulations exist in other regions:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Region<\/th>\n<th>Regulation<\/th>\n<th>Deep Link Impact<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>EU\/EEA<\/td>\n<td><a href=\"https:\/\/eur-lex.europa.eu\/legal-content\/EN\/TXT\/?uri=celex%3A32015L2366\" rel=\"nofollow noopener\" target=\"_blank\">PSD2<\/a><\/td>\n<td>SCA redirects, TPP consent flows<\/td>\n<\/tr>\n<tr>\n<td>UK<\/td>\n<td><a href=\"https:\/\/www.openbanking.org.uk\/\" rel=\"nofollow noopener\" target=\"_blank\">UK Open Banking<\/a><\/td>\n<td>Similar to PSD2, uses UK-specific standards<\/td>\n<\/tr>\n<tr>\n<td>Australia<\/td>\n<td><a href=\"https:\/\/www.cdr.gov.au\/\" rel=\"nofollow noopener\" target=\"_blank\">CDR<\/a> (Consumer Data Right)<\/td>\n<td>Data sharing consent flows<\/td>\n<\/tr>\n<tr>\n<td>Brazil<\/td>\n<td><a href=\"https:\/\/openfinancebrasil.org.br\/\" rel=\"nofollow noopener\" target=\"_blank\">Open Finance Brazil<\/a><\/td>\n<td>Payment initiation and consent<\/td>\n<\/tr>\n<tr>\n<td>India<\/td>\n<td><a href=\"https:\/\/sahamati.org.in\/\" rel=\"nofollow noopener\" target=\"_blank\">Account Aggregator<\/a><\/td>\n<td>Consent-based data sharing<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for PSD2 Flows<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> supports the route patterns PSD2 redirect flows need. Define callback routes in the <a href=\"https:\/\/tolinku.com\/docs\/concepts\/deep-linking\/\">Tolinku dashboard<\/a>. Universal Links and App Links ensure that PSD2 redirects open the correct app reliably on both iOS and Android.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For fintech compliance, see <a href=\"https:\/\/tolinku.com\/blog\/fintech-compliance-deep-links\/\">fintech compliance and deep links<\/a>. For fintech deep linking broadly, see <a href=\"https:\/\/tolinku.com\/blog\/deep-linking-fintech-banking-apps\/\">deep linking for fintech and banking apps<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Handle PSD2 and open banking requirements with deep links. Manage strong customer authentication (SCA) and third-party provider redirects in fintech apps.<\/p>\n","protected":false},"author":2,"featured_media":1706,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Deep Linking and PSD2: Open Banking Considerations","rank_math_description":"Handle PSD2 and open banking requirements with deep links. Manage strong customer authentication and third-party provider redirects.","rank_math_focus_keyword":"deep linking PSD2","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-fintech-deep-linking-psd2.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-fintech-deep-linking-psd2.png","footnotes":""},"categories":[18],"tags":[129,20,59,69,511,36,512,93],"class_list":["post-1707","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-use-cases","tag-compliance","tag-deep-linking","tag-fintech","tag-mobile-development","tag-open-banking","tag-privacy","tag-psd2","tag-security"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1707","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1707"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1707\/revisions"}],"predecessor-version":[{"id":2682,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1707\/revisions\/2682"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1706"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1707"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1707"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1707"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}