--- title: Handle offsite payments description: >- Configure Universal Links and App Links to bring buyers back to your app after offsite payments. source_url: html: 'https://shopify.dev/docs/storefronts/mobile/checkout-kit/offsite-payments' md: 'https://shopify.dev/docs/storefronts/mobile/checkout-kit/offsite-payments.md' --- # Handle offsite payments Some payment providers redirect buyers outside your app to complete a transaction, either to a web page or an external banking app. To bring buyers back to your app, configure Universal Links (iOS) and App Links (Android) on your storefront domain. *** ## Requirements * [Checkout Kit](https://shopify.dev/docs/storefronts/mobile/checkout-kit) installed in your app. * A [custom domain](https://admin.shopify.com/settings/domains) connected to your storefront. Universal Links and App Links aren't available on `*.myshopify.com` domains. * HTTPS enabled on the custom domain. * A [custom app](https://shopify.dev/docs/apps/build) with [Storefront API access](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started). * [Xcode](https://developer.apple.com/xcode/) to configure Associated Domains for iOS. * Optional: [Android Studio](https://developer.android.com/studio) to verify App Links with the App Links Assistant. *** ## Step 1: Configure your storefront Set up your storefront to generate the `.well-known` files that iOS and Android use to verify your app owns the domain. 1. Go to your [app's configuration in the Shopify admin](https://admin.shopify.com/settings/apps/development). 2. Click the name of your app. 3. Click **API Integrations**. 4. In the **Storefront API integration** section, click **Configure**. 5. Configure link handling for your platforms: * **iOS**: Expand the **iOS Buy SDK configuration** section, enter your Apple App ID, and select **Use iOS universal links**. * **Android**: Expand the **Android Buy SDK configuration** section and provide your Android application ID and SHA256 fingerprint certificate values from the Play Store. This generates two `.well-known` JSON files on your custom domain: * `apple-app-site-association` for iOS * `assetlinks.json` for Android ## .well-known/apple-app-site-association (iOS) ```json { "applinks": { "apps": [], "details": [ { "appID": "{APPLE_TEAM_ID}.{BUNDLE_IDENTIFIER}", "paths": [ "NOT /admin/*", "NOT /*/amazon_payments/callback", "NOT /*/checkouts/*/express/callback", "/*" ] } ] } } ``` ## .well-known/assetlinks.json (Android) ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "{PACKAGE_NAME}", "sha256_cert_fingerprints": [ "{SHA256_FINGERPRINT}" ] } } ] ``` The `/*` catch-all in `apple-app-site-association` tells iOS to open any URL from your domain in your app, except paths excluded by `NOT` rules. The `handle_all_urls` relation in `assetlinks.json` does the same for Android. iOS periodically re-fetches these files. Android verifies them at install time and on app updates. This affects all links from your domain: * **Email links**: Links in emails (such as abandoned carts) open in your app. * **Shared links**: Web links from your domain open in your app unless excluded. * **Web view links**: Links opened in a web view don't trigger the app unless you configure the web view to handle them. * **Excluded paths**: (iOS only) URLs with a `NOT` clause open in the browser. *** ## Step 2: Enable linking in your app ### Configure Universal Links for i​OS Add an `entitlements` file that specifies the domains your app handles: 1. Open your project in Xcode. 2. Select your app target, then click **Signing & Capabilities**. 3. Click **+ Capability** and add **Associated Domains**. 4. Add each domain in the format `applinks:example.com`: ## Example.entitlements ```xml com.apple.developer.associated-domains applinks:example.com applinks:subdomain.example.com ``` ### Configure App Links for Android Add an intent filter for your domain to `AndroidManifest.xml`, then handle incoming intents in your activity: ## AndroidManifest.xml ```xml ``` ### Configure linking for React Native React Native apps need both platform configurations. Apply the iOS steps to `App.entitlements` and the Android steps to `AndroidManifest.xml`. *** ## Step 3: Route incoming URLs Update your app's entry point to route incoming URLs: * `/checkout` URLs go to Checkout Kit. * `/cart` URLs open your cart screen. * Unrecognized URLs fall back to the mobile browser so buyers can complete their purchase. ##### Swift (SwiftUI) ```swift // App.swift import SwiftUI import ShopifyCheckoutSheetKit @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in handleUniversalLink(url: url) } } } private func handleUniversalLink(url: URL) { let storefrontUrl = StorefrontURL(from: url) switch true { case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage(): presentCheckout(url) case storefrontUrl.isCart(): navigateToCart() default: if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } } } struct ContentView: View { var body: some View { Text("Hello, World!") } } public struct StorefrontURL { public let url: URL init(from url: URL) { self.url = url } public func isThankYouPage() -> Bool { return url.path.range(of: "/thank[-_]you", options: .regularExpression) != nil } public func isCheckout() -> Bool { return url.path.contains("/checkouts/") } public func isCart() -> Bool { return url.path.contains("/cart") } } ``` ##### Swift (UIKit) ```swift // AppDelegate.swift import UIKit import ShopifyCheckoutSheetKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { handleUniversalLink(url: url) return true } return false } private func handleUniversalLink(url: URL) { let storefrontUrl = StorefrontURL(from: url) switch true { case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage(): presentCheckout(url) case storefrontUrl.isCart(): navigateToCart() default: if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } } } public struct StorefrontURL { public let url: URL init(from url: URL) { self.url = url } public func isThankYouPage() -> Bool { return url.path.range(of: "/thank[-_]you", options: .regularExpression) != nil } public func isCheckout() -> Bool { return url.path.contains("/checkouts/") } public func isCart() -> Bool { return url.path.contains("/cart") } } ``` ##### Kotlin ```kotlin // MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { App() } handleIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) } private fun handleIntent(intent: Intent) { if (intent.action == Intent.ACTION_VIEW) { intent.data?.let { uri -> when { uri.path?.contains("/checkouts/") == true -> { // Route checkout URLs to Checkout Kit ShopifyCheckoutSheetKit.present(uri.toString(), this, checkoutEventProcessor) } uri.path == "/cart" -> navigateToCart() else -> { // Open unrecognized URLs in the browser startActivity(Intent(Intent.ACTION_VIEW, uri)) } } } } } private fun navigateToCart() { // Navigate to the cart screen } } ``` ##### React Native ```tsx // src/App.tsx import React, { useState, useEffect } from 'react'; import { Linking } from 'react-native'; import { useNavigation, NavigationProp } from '@react-navigation/native'; import { useShopifyCheckoutSheet } from '@shopify/checkout-sheet-kit'; type RootStackParamList = { Cart: undefined; }; const useInitialURL = (): {url: string | null} => { const [url, setUrl] = useState(null); useEffect(() => { const getUrlAsync = async () => { const initialUrl = await Linking.getInitialURL(); if (initialUrl !== url) { setUrl(initialUrl); } }; getUrlAsync(); }, [url]); return { url, }; }; function Routes() { const shopify = useShopifyCheckoutSheet(); const navigation = useNavigation>(); const {url: initialUrl} = useInitialURL(); useEffect(() => { async function handleUniversalLink(url: string) { const storefrontUrl = new StorefrontURL(url); switch (true) { case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage(): shopify.present(url); return; case storefrontUrl.isCart(): navigation.navigate('Cart'); return; } const canOpenUrl = await Linking.canOpenURL(url); if (canOpenUrl) { await Linking.openURL(url); } } if (initialUrl) { handleUniversalLink(initialUrl); } const subscription = Linking.addEventListener('url', ({url}) => { handleUniversalLink(url); }); return () => { subscription.remove(); }; }, [initialUrl, shopify, navigation]); return ( // Route components ); } class StorefrontURL { readonly url: string; constructor(url: string) { this.url = url; } isThankYouPage(): boolean { return /thank[-_]you/i.test(this.url); } isCheckout(): boolean { return this.url.includes('/checkouts/'); } isCart() { return this.url.includes('/cart'); } } ``` *** ## Step 4: Test your configuration ### Validate `well-known` files Verify that your custom domain serves the `.well-known` files. The response returns valid JSON containing your app ID (iOS) or package name (Android). If you get a 404 or empty response, then check that your custom domain is configured correctly: ##### iOS ```sh curl https://example.com/.well-known/apple-app-site-association | jq . ``` ##### Android ```sh curl https://example.com/.well-known/assetlinks.json | jq . ``` ### Simulate links in a terminal Trigger URLs manually to test linking in a simulator or emulator. Your app opens and routes to the appropriate screen. If the browser opens instead, then check your entitlements (iOS) or intent filters (Android): ##### iOS ```sh xcrun simctl openurl booted "https://www.example.com/cart" ``` ##### Android ```sh adb shell am start -a android.intent.action.VIEW -d "https://example.com/cart" ``` ### i​OS diagnostics Open **Settings** on a physical device (not a simulator) and search for **Universal Links**. Look for your domain in the diagnostics list with a green checkmark. If it shows a warning or doesn't appear, then reinstall the app: | Settings > Developer | Universal Links > Diagnostics | Link configured | | - | - | - | | ![iOS Settings showing the Developer section.](https://shopify.dev/assets/assets/images/custom-storefronts/checkout-sheet-kit/settings-universal-links-DYQ6G1RD.png) | ![Universal Links diagnostics screen showing associated domains.](https://shopify.dev/assets/assets/images/custom-storefronts/checkout-sheet-kit/universal-links-diagnostics-InM2nxpp.png) | ![A configured Universal Link pointing to the storefront domain.](https://shopify.dev/assets/assets/images/custom-storefronts/checkout-sheet-kit/universal-link-configured-BeQjEaT0.png) | ### Android diagnostics Use the **App Links Assistant** in Android Studio to verify your app links. ![Android Studio App Links Assistant showing verified app links.](https://shopify.dev/assets/assets/images/custom-storefronts/checkout-sheet-kit/app-links-assistant-B2w4-oqr.png) *** ## Next steps [Privacy compliance\ \ ](https://shopify.dev/docs/storefronts/mobile/checkout-kit/privacy-compliance) [Pass visitor consent for GDPR, CCPA, and App Tracking Transparency.](https://shopify.dev/docs/storefronts/mobile/checkout-kit/privacy-compliance) [About accelerated checkouts\ \ ](https://shopify.dev/docs/storefronts/mobile/checkout-kit/accelerated-checkouts-overview) [Add Shop Pay and Apple Pay buttons for one-tap purchases on product and cart pages.](https://shopify.dev/docs/storefronts/mobile/checkout-kit/accelerated-checkouts-overview) ***