Custom URL schemes allow applications to register unique protocols for deep linking. They enable web-to-app and app-to-app communication.
Deep linking with custom URL schemes creates seamless transitions between web and native apps. When a user taps slack://channel?id=C123, they land directly in the right Slack channel—no navigation required. Understanding how to implement and secure custom schemes is essential for mobile and desktop app development.
Key Takeaways
- 1Custom schemes use yourapp:// prefix
- 2Apps must register to handle schemes
- 3Universal Links (iOS) and App Links (Android) are preferred
- 4Scheme hijacking is a security risk
- 5Use HTTPS for secure app linking when possible
"Custom URL schemes provide a way to reference resources inside your app. Users tapping a custom URL in an email, for example, launch your app in a specified context."
Custom Scheme Structure
Custom schemes follow the same structure as standard URLs—scheme, host, path, query, and fragment. The scheme name is what makes them unique and triggers your app instead of a browser.
# Custom URL scheme format
scheme://[host]/path[?query][#fragment]
# Examples:
myapp://open
myapp://home
myapp://product/12345
myapp://action?param=value
slack://channel?id=C12345
spotify://track/abc123
twitter://user?screen_name=example
fb://profile/123456789
# With path and query
myapp://products/view?id=123&source=email
# Common patterns:
{app}:// # Open app
{app}://path/to/content # Deep link
{app}://action?params # Action with dataThe URL structure is flexible—you define what the host, path, and query parameters mean for your app. Common patterns include using the host as an action name, the path for resource IDs, and query parameters for additional context.
Many popular apps have established custom schemes you can use to integrate with them.
Common App Schemes
This table shows URL schemes for popular applications. These are useful when you want to deep link users directly into specific content within other apps.
| App | Scheme | Example |
|---|---|---|
| Slack | slack:// | slack://channel?team=T123&id=C456 |
| Spotify | spotify:// | spotify://track/abc123 |
| Twitter/X | twitter:// | twitter://user?screen_name=name |
| fb:// | fb://profile/123456 | |
| instagram:// | instagram://user?username=name | |
| Zoom | zoomus:// | zoomus://zoom.us/join?confno=123 |
| whatsapp:// | whatsapp://send?text=Hello | |
| Telegram | tg:// | tg://msg?text=Hello |
Note that app schemes can change and may not work if the app isn't installed. For critical functionality, consider fallback handling or use Universal Links / App Links instead.
Let's look at how to register and handle custom schemes on iOS.
iOS Custom Schemes
iOS apps register URL schemes in their Info.plist configuration. When iOS encounters a URL with your scheme, it launches your app and passes the URL for handling.
<!-- iOS: Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
</dict>
</array>The CFBundleURLSchemes array lists all schemes your app handles. You can register multiple schemes if needed. The CFBundleURLName should be your reverse-domain identifier.
// iOS: Handle URL in AppDelegate or SceneDelegate
// AppDelegate
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// url = myapp://product/123?color=blue
guard url.scheme == "myapp" else { return false }
let host = url.host // "product"
let path = url.path // "/123"
let query = url.query // "color=blue"
// Parse and handle
handleDeepLink(host: host, path: path, query: query)
return true
}
// SceneDelegate (iOS 13+)
func scene(_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
handleDeepLink(url: url)
}
// SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
handleDeepLink(url: url)
}
}
}
}iOS provides three ways to handle URLs depending on your app architecture: the traditional AppDelegate, the SceneDelegate for iOS 13+, and the SwiftUI onOpenURL modifier. Parse the URL components and route to the appropriate content in your app.
Android uses a different approach with intent filters in the manifest.
Android Custom Schemes
Android registers URL handlers through intent filters in AndroidManifest.xml. You can handle all URLs with your scheme or filter by specific hosts and paths.
<!-- Android: AndroidManifest.xml -->
<activity android:name=".DeepLinkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
<!-- Specific paths -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp"
android:host="product"
android:pathPrefix="/" />
</intent-filter>
</activity>The android:exported="true" attribute is required for activities that handle external URLs. The BROWSABLE category allows the intent to be triggered from a browser link.
// Android: Handle URL in Activity
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.data?.let { uri ->
// uri = myapp://product/123?color=blue
val scheme = uri.scheme // "myapp"
val host = uri.host // "product"
val path = uri.path // "/123"
val color = uri.getQueryParameter("color") // "blue"
handleDeepLink(host, path, color)
}
}
}
// With Navigation Component
val navController = findNavController(R.id.nav_host)
navController.handleDeepLink(intent)The Kotlin code extracts URL components using Android's Uri class. If you're using the Navigation component, it can automatically route deep links to the appropriate destination based on your navigation graph.
From the web side, you need to create links that trigger app launches and handle cases where the app isn't installed.
Web Implementation
Linking to custom schemes from web pages is straightforward, but detecting whether the app is installed and handling fallbacks requires some workarounds due to browser security restrictions.
<!-- Link to custom scheme -->
<a href="myapp://product/123">Open in App</a>
<!-- With fallback -->
<a href="myapp://product/123"
onclick="setTimeout(() => window.location.href = 'https://myapp.com/product/123', 500)">
Open in App
</a>// Smart app banner (iOS)
// <meta name="apple-itunes-app" content="app-id=123456789, app-argument=myapp://product/123">
// Detect if app is installed (unreliable)
function openApp(appUrl, webUrl, timeout = 2500) {
const start = Date.now();
// Try to open app
window.location.href = appUrl;
// If we're still here after timeout, redirect to web
setTimeout(() => {
if (Date.now() - start < timeout + 100) {
window.location.href = webUrl;
}
}, timeout);
}
// Usage
openApp(
'myapp://product/123',
'https://myapp.com/product/123'
);
// Modern approach: Universal Links (iOS) / App Links (Android)
// These use HTTPS URLs that open the app if installed
// Fallback to web automatically
<a href="https://myapp.com/product/123">View Product</a>The timeout-based detection attempts to open the app, then falls back to the web URL if we're still on the page after a delay. This is unreliable—the better solution is Universal Links (iOS) or App Links (Android), which use HTTPS URLs that automatically open the app when installed.
For desktop applications built with Electron, you can register custom schemes to handle URLs from other applications.
Electron Custom Scheme
Electron provides cross-platform APIs for registering as the default handler for a custom scheme. URL handling differs between macOS (event-based) and Windows/Linux (command-line argument).
// Electron: Register custom protocol handler
const { app, protocol } = require('electron');
// macOS: Set as default handler
app.setAsDefaultProtocolClient('myapp');
// Handle URL on macOS
app.on('open-url', (event, url) => {
event.preventDefault();
handleDeepLink(url);
});
// Handle URL on Windows/Linux (single instance)
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine) => {
// Windows: URL is in commandLine
const url = commandLine.find(arg => arg.startsWith('myapp://'));
if (url) {
handleDeepLink(url);
}
});
}
function handleDeepLink(url) {
const parsed = new URL(url);
console.log('Deep link:', parsed.pathname, parsed.searchParams);
}The single instance lock ensures only one instance of your app runs—if the user clicks a link when the app is already open, the second-instance event fires with the URL. On macOS, the open-url event handles this case instead.
Custom URL schemes have several security vulnerabilities you need to understand and mitigate.
Security Considerations
Unlike HTTPS URLs, custom schemes have no built-in verification mechanism. This creates opportunities for attacks that you must defend against through careful implementation.
| Risk | Description | Mitigation |
|---|---|---|
| Scheme hijacking | Malicious app registers your scheme | Use Universal/App Links |
| Data exposure | Sensitive data in URL | Don't include secrets in URLs |
| Phishing | Fake app handles your URLs | Validate app identity with HTTPS |
| Injection | Malformed URL data | Validate and sanitize all URL parts |
Scheme hijacking is the most serious concern—any app can claim your scheme, potentially intercepting your users' data. Universal Links and App Links solve this by requiring you to prove domain ownership, ensuring only your app handles your URLs.