Skip to content
Tolinku
Tolinku
Sign In Start Free
Engineering · · 5 min read

Capacitor Deep Linking: Setup for Ionic Apps

By Tolinku Staff
|
Tolinku cross platform dashboard screenshot for engineering blog posts

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):

  1. Select the App target.
  2. Go to Signing & Capabilities.
  3. Click "+ Capability" and add "Associated Domains."
  4. 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>

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 appUrlOpen event 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.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.