Capacitor apps are web apps wrapped in a native shell. Deep linking in Capacitor requires native configuration (AASA, assetlinks.json, entitlements, Intent filters) plus JavaScript-side URL handling. This guide covers both sides.
For the cross-platform deep linking overview, see cross-platform deep linking guide for 2026. For Ionic-specific configuration, see Ionic deep linking: complete configuration guide.
Prerequisites
- Capacitor 5+ (current LTS)
- An HTTPS domain you control
- AASA file hosted at
/.well-known/apple-app-site-association - assetlinks.json hosted at
/.well-known/assetlinks.json
iOS Configuration
1. Add Associated Domains Entitlement
In Xcode, open your project (the ios/App directory):
- Select the App target.
- Go to Signing & Capabilities.
- Click "+ Capability" and add "Associated Domains."
- Add your domain:
applinks:yourdomain.com
This creates or updates the .entitlements file:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:yourdomain.com</string>
</array>
2. AASA File
Host at https://yourdomain.com/.well-known/apple-app-site-association:
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.yourcompany.yourapp"],
"components": [
{ "/": "/products/*" },
{ "/": "/offers/*" },
{ "/": "/referral/*" }
]
}
]
}
}
3. Handle the URL in AppDelegate
Capacitor's default AppDelegate.swift already includes the Universal Links handler. Verify it exists:
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return CAPBridge.handleContinueActivity(userActivity, restorationHandler: restorationHandler)
}
This forwards Universal Link URLs to Capacitor's bridge, which makes them available to your JavaScript code.
Android Configuration
1. Intent Filters
Edit android/app/src/main/AndroidManifest.xml:
<activity
android:name="com.yourcompany.yourapp.MainActivity"
android:exported="true">
<!-- Existing intent filters... -->
<!-- App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/products" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/offers" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/referral" />
</intent-filter>
</activity>
2. assetlinks.json
Host at https://yourdomain.com/.well-known/assetlinks.json:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"YOUR_SIGNING_KEY_FINGERPRINT"
]
}
}
]
Get your fingerprint:
# Debug key
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
# Release key (Play App Signing)
# Find in Google Play Console > Setup > App signing
JavaScript-Side Handling
Using the App Plugin
Capacitor's built-in @capacitor/app plugin provides the appUrlOpen event:
import { App } from '@capacitor/app';
import { Router } from '@angular/router'; // or your framework's router
// Listen for deep link opens
App.addListener('appUrlOpen', (event) => {
console.log('Deep link URL:', event.url);
const url = new URL(event.url);
const path = url.pathname;
const params = Object.fromEntries(url.searchParams);
// Route to the correct page
handleDeepLink(path, params);
});
function handleDeepLink(path: string, params: Record<string, string>) {
// Match routes
const productMatch = path.match(/^\/products\/([^/]+)$/);
if (productMatch) {
router.navigate(['/product', productMatch[1]], { queryParams: params });
return;
}
const offerMatch = path.match(/^\/offers\/([^/]+)$/);
if (offerMatch) {
router.navigate(['/offer', offerMatch[1]], { queryParams: params });
return;
}
const referralMatch = path.match(/^\/referral\/([^/]+)$/);
if (referralMatch) {
router.navigate(['/referral', referralMatch[1]], { queryParams: params });
return;
}
// Default: go home
router.navigate(['/']);
}
Handling Cold Start vs Warm Start
Deep links can arrive when the app is:
- Cold start: App is not running. The URL is available when the app initializes.
- Warm start: App is in the background. The
appUrlOpenevent fires.
Handle the cold start case by checking for a URL on app initialization:
import { App } from '@capacitor/app';
async function initializeApp() {
// Check if the app was launched from a deep link
const launchUrl = await App.getLaunchUrl();
if (launchUrl?.url) {
console.log('App launched from deep link:', launchUrl.url);
handleDeepLink(new URL(launchUrl.url).pathname, {});
}
// Listen for subsequent deep links (warm start)
App.addListener('appUrlOpen', (event) => {
handleDeepLink(new URL(event.url).pathname, {});
});
}
With Angular Router
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { App } from '@capacitor/app';
@Injectable({ providedIn: 'root' })
export class DeepLinkService {
constructor(private router: Router) {}
initialize() {
// Cold start
App.getLaunchUrl().then(result => {
if (result?.url) this.route(result.url);
});
// Warm start
App.addListener('appUrlOpen', event => {
this.route(event.url);
});
}
private route(urlString: string) {
const url = new URL(urlString);
// Angular router can handle the path directly
this.router.navigateByUrl(url.pathname + url.search);
}
}
With React (Capacitor + React)
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { App } from '@capacitor/app';
function useDeepLinks() {
const navigate = useNavigate();
useEffect(() => {
// Cold start
App.getLaunchUrl().then(result => {
if (result?.url) {
const url = new URL(result.url);
navigate(url.pathname + url.search);
}
});
// Warm start
const listener = App.addListener('appUrlOpen', event => {
const url = new URL(event.url);
navigate(url.pathname + url.search);
});
return () => {
listener.then(l => l.remove());
};
}, [navigate]);
}
Testing
iOS
# Test on iOS Simulator
xcrun simctl openurl booted "https://yourdomain.com/products/123"
# If the app opens and navigates to the product page, it works
Android
# Test on connected device/emulator
adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/products/123" \
-c android.intent.category.BROWSABLE
In the Browser (Development)
During development, you can test routing logic in the browser by navigating directly to the path:
http://localhost:8100/products/123
This tests the routing logic but not the native deep link handling.
Common Issues
| Issue | Cause | Fix |
|---|---|---|
appUrlOpen not firing |
Missing Associated Domains entitlement | Add applinks:yourdomain.com in Xcode |
| App opens but home screen shows | Route handler not matching the path | Debug the URL parsing logic |
| Works on Android but not iOS | AASA file issue | Check AASA with curl and Apple's validator |
| Works from Notes but not email | Email client wraps the link | Use a custom tracking domain |
Tolinku for Capacitor Apps
Tolinku's web SDK works with Capacitor apps for smart banners and analytics. Tolinku hosts your AASA and assetlinks.json files automatically.
For web-to-app deep linking, see deep linking for web: bridging browser and app experiences. For the cross-platform guide, see cross-platform deep linking guide for 2026.
Get deep linking tips in your inbox
One email per week. No spam.