Custom URL schemes (myapp://products/123) were the original way to open a specific screen in a mobile app. They are easy to implement but fundamentally flawed: no ownership verification, no fallback, and no security. This guide covers when they are still useful, how to set them up, and when to migrate to Universal Links and App Links instead.
For the comparison between URL schemes and Universal Links, see URI schemes vs Universal Links: which should you use?. For a complete deep linking overview, see the complete guide to deep linking in 2026.
How Custom URL Schemes Work
A custom URL scheme registers a prefix (like myapp://) with the operating system. When any app or browser opens a URL with that prefix, the OS launches the registered app and passes the full URL.
myapp://products/123
↓
OS looks up which app registered "myapp://"
↓
Launches that app with the URL
↓
App parses the URL and navigates to ProductDetail(id: 123)
iOS Setup
Registering the Scheme
In your Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourcompany.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Or in Xcode: Target > Info > URL Types > Add a URL scheme.
Handling the URL
UIKit (AppDelegate):
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
guard url.scheme == "myapp" else { return false }
let host = url.host // e.g., "products"
let path = url.path // e.g., "/123"
let query = url.query // e.g., "ref=email"
return router.handle(host: host, path: path, query: query)
}
SwiftUI:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
router.handle(url)
}
}
}
}
Testing on iOS
# Open a custom URL scheme in the Simulator
xcrun simctl openurl booted "myapp://products/123"
Or in Safari on a real device, type myapp://products/123 in the address bar (Safari will ask "Open in MyApp?").
Android Setup
Registering the Scheme
In your 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>
</activity>
Handling the Intent
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val data = intent.data ?: return
val host = data.host // e.g., "products"
val path = data.path // e.g., "/123"
val query = data.query // e.g., "ref=email"
router.handle(host, path, query)
}
}
Testing on Android
# Open a custom URL scheme on a connected device
adb shell am start -a android.intent.action.VIEW \
-d "myapp://products/123" \
-c android.intent.category.BROWSABLE
Limitations of Custom URL Schemes
1. No Ownership Verification
Any app can register any URL scheme. There is no verification that myapp:// belongs to your company. If another app registers the same scheme:
- iOS: The behavior is undefined. One of the apps will handle the URL, but you cannot predict which one.
- Android: The OS shows a disambiguation dialog asking the user to choose.
This is a security risk. A malicious app could register your scheme and intercept sensitive URLs (password reset tokens, payment confirmations).
2. No Fallback
If the app is not installed, custom URL schemes fail:
- iOS: Nothing happens, or the user sees a confusing "Safari cannot open the page" error.
- Android: The user sees "No app found to handle this link."
There is no way to fall back to a website because myapp:// is not an HTTP URL.
3. Browser Restrictions
Modern browsers restrict custom URL schemes:
| Browser | Behavior |
|---|---|
| Safari (iOS 17+) | Prompts "Open in MyApp?" (user must confirm) |
| Chrome (Android) | May block scheme redirects from web pages |
| Chrome (iOS) | Shows a confirmation dialog |
| Firefox | Blocks most custom scheme navigations |
4. No Click Tracking
Email service providers, social media platforms, and analytics tools cannot track clicks on myapp:// URLs. They only track HTTP/HTTPS URLs.
5. No SEO Value
Custom URL schemes are not indexable by search engines. Universal Links, being standard HTTPS URLs, can be indexed and contribute to SEO.
When Custom URL Schemes Are Still Useful
Despite the limitations, custom URL schemes have valid use cases:
App-to-App Communication
If your company has multiple apps, custom URL schemes provide a simple way for them to communicate:
// From App A, open App B to a specific screen
if let url = URL(string: "otherapp://settings/notifications") {
UIApplication.shared.open(url)
}
This is safe because both apps are under your control. The security risk of scheme hijacking is minimal.
Push Notification Deep Links
Push notifications are handled by the app itself, so the URL does not need to go through a browser or the OS's Universal Links system:
{
"aps": {
"alert": "Your order shipped!"
},
"deep_link": "myapp://orders/456/tracking"
}
The app receives the notification, extracts the deep_link field, and navigates to the tracking screen. No need for AASA or domain verification.
Legacy Support
If you have existing deep links in the wild (printed on physical media, embedded in older emails), you cannot change them. Continue supporting the custom scheme while also implementing Universal Links/App Links for new links.
Migrating to Universal Links / App Links
The Migration Path
- Implement Universal Links (iOS) and App Links (Android) alongside your existing custom scheme.
- Update all new link generation to use HTTPS URLs.
- Keep the custom scheme handler for backward compatibility.
- Redirect old scheme URLs to the new HTTPS format in your app's route handler.
func handleURL(_ url: URL) {
if url.scheme == "myapp" {
// Convert custom scheme to Universal Link format
let httpsUrl = URL(string: "https://app.example.com/\(url.host ?? "")\(url.path)")!
router.handle(httpsUrl)
} else {
// Already a Universal Link
router.handle(url)
}
}
What to Migrate First
| Priority | Action |
|---|---|
| High | Stop generating custom scheme URLs in email campaigns |
| High | Switch social media share links to HTTPS |
| Medium | Update SDK/API to return HTTPS deep links |
| Low | Update push notification payloads (custom schemes work fine here) |
| Do not | Remove custom scheme registration (breaks backward compatibility) |
Custom Scheme Best Practices
If you must use custom URL schemes:
- Use a unique, specific scheme.
mycompanyappname://is better thanmyapp://because it is less likely to conflict. - Validate all input. Treat the URL as untrusted input. A malicious app could open
myapp://delete-accountif you are not careful. - Do not put sensitive data in the URL. Tokens, passwords, or API keys in a custom scheme URL can be intercepted by other apps.
- Always implement a fallback. Check
UIApplication.shared.canOpenURL()before opening a custom scheme to avoid silent failures.
let url = URL(string: "otherapp://action")!
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
} else {
// Fallback: open the App Store or a web page
UIApplication.shared.open(URL(string: "https://apps.apple.com/app/otherapp/id123456")!)
}
Note: on iOS, you must declare the schemes you want to query in your Info.plist under LSApplicationQueriesSchemes, limited to 50 entries (Apple documentation).
Tolinku for Deep Linking
Tolinku uses Universal Links and App Links (HTTPS-based deep linking) with automatic AASA and assetlinks.json hosting, eliminating the need for custom URL schemes. See the deep linking concepts for how Tolinku handles link resolution.
For the comparison, see URI schemes vs Universal Links: which should you use?. For the technical overview, see how deep linking works: a technical overview.
Get deep linking tips in your inbox
One email per week. No spam.