{"id":1833,"date":"2026-07-24T09:00:00","date_gmt":"2026-07-24T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1833"},"modified":"2026-03-07T03:50:11","modified_gmt":"2026-03-07T08:50:11","slug":"custom-url-schemes-guide","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/custom-url-schemes-guide\/","title":{"rendered":"Custom URL Schemes for Mobile Apps: Setup and Pitfalls"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Custom URL schemes (<code>myapp:\/\/products\/123<\/code>) 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the comparison between URL schemes and Universal Links, see <a href=\"https:\/\/tolinku.com\/blog\/uri-schemes-vs-universal-links\/\">URI schemes vs Universal Links: which should you use?<\/a>. For a complete deep linking overview, see <a href=\"https:\/\/tolinku.com\/blog\/complete-guide-deep-linking-2026\/\">the complete guide to deep linking in 2026<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Custom URL Schemes Work<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A custom URL scheme registers a prefix (like <code>myapp:\/\/<\/code>) 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.<\/p>\n\n\n\n<pre><code>myapp:\/\/products\/123\n  \u2193\nOS looks up which app registered &quot;myapp:\/\/&quot;\n  \u2193\nLaunches that app with the URL\n  \u2193\nApp parses the URL and navigates to ProductDetail(id: 123)\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">iOS Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Registering the Scheme<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In your <code>Info.plist<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;key&gt;CFBundleURLTypes&lt;\/key&gt;\n&lt;array&gt;\n  &lt;dict&gt;\n    &lt;key&gt;CFBundleURLName&lt;\/key&gt;\n    &lt;string&gt;com.yourcompany.myapp&lt;\/string&gt;\n    &lt;key&gt;CFBundleURLSchemes&lt;\/key&gt;\n    &lt;array&gt;\n      &lt;string&gt;myapp&lt;\/string&gt;\n    &lt;\/array&gt;\n  &lt;\/dict&gt;\n&lt;\/array&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or in Xcode: Target &gt; Info &gt; URL Types &gt; Add a URL scheme.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Handling the URL<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>UIKit (AppDelegate):<\/strong><\/p>\n\n\n\n<pre><code class=\"language-swift\">func application(_ app: UIApplication,\n                 open url: URL,\n                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -&gt; Bool {\n    guard url.scheme == &quot;myapp&quot; else { return false }\n\n    let host = url.host          \/\/ e.g., &quot;products&quot;\n    let path = url.path          \/\/ e.g., &quot;\/123&quot;\n    let query = url.query        \/\/ e.g., &quot;ref=email&quot;\n\n    return router.handle(host: host, path: path, query: query)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SwiftUI:<\/strong><\/p>\n\n\n\n<pre><code class=\"language-swift\">@main\nstruct MyApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n                .onOpenURL { url in\n                    router.handle(url)\n                }\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Testing on iOS<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Open a custom URL scheme in the Simulator\nxcrun simctl openurl booted &quot;myapp:\/\/products\/123&quot;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or in Safari on a real device, type <code>myapp:\/\/products\/123<\/code> in the address bar (Safari will ask &quot;Open in MyApp?&quot;).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Android Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Registering the Scheme<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In your <code>AndroidManifest.xml<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity android:name=&quot;.DeepLinkActivity&quot;\n          android:exported=&quot;true&quot;&gt;\n    &lt;intent-filter&gt;\n        &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n        &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n        &lt;data android:scheme=&quot;myapp&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Handling the Intent<\/h3>\n\n\n\n<pre><code class=\"language-kotlin\">class DeepLinkActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val data = intent.data ?: return\n        val host = data.host       \/\/ e.g., &quot;products&quot;\n        val path = data.path       \/\/ e.g., &quot;\/123&quot;\n        val query = data.query     \/\/ e.g., &quot;ref=email&quot;\n\n        router.handle(host, path, query)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Testing on Android<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Open a custom URL scheme on a connected device\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;myapp:\/\/products\/123&quot; \\\n  -c android.intent.category.BROWSABLE\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Limitations of Custom URL Schemes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. No Ownership Verification<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Any app can register any URL scheme. There is no verification that <code>myapp:\/\/<\/code> belongs to your company. If another app registers the same scheme:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>iOS:<\/strong> The behavior is undefined. One of the apps will handle the URL, but you cannot predict which one.<\/li>\n<li><strong>Android:<\/strong> The OS shows a disambiguation dialog asking the user to choose.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This is a security risk. A malicious app could register your scheme and intercept sensitive URLs (password reset tokens, payment confirmations).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. No Fallback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If the app is not installed, custom URL schemes fail:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>iOS:<\/strong> Nothing happens, or the user sees a confusing &quot;Safari cannot open the page&quot; error.<\/li>\n<li><strong>Android:<\/strong> The user sees &quot;No app found to handle this link.&quot;<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">There is no way to fall back to a website because <code>myapp:\/\/<\/code> is not an HTTP URL.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Browser Restrictions<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Modern browsers restrict custom URL schemes:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Browser<\/th>\n<th>Behavior<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Safari (iOS 17+)<\/td>\n<td>Prompts &quot;Open in MyApp?&quot; (user must confirm)<\/td>\n<\/tr>\n<tr>\n<td>Chrome (Android)<\/td>\n<td>May block scheme redirects from web pages<\/td>\n<\/tr>\n<tr>\n<td>Chrome (iOS)<\/td>\n<td>Shows a confirmation dialog<\/td>\n<\/tr>\n<tr>\n<td>Firefox<\/td>\n<td>Blocks most custom scheme navigations<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">4. No Click Tracking<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Email service providers, social media platforms, and analytics tools cannot track clicks on <code>myapp:\/\/<\/code> URLs. They only track HTTP\/HTTPS URLs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. No SEO Value<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Custom URL schemes are not indexable by search engines. Universal Links, being standard HTTPS URLs, can be indexed and contribute to SEO.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When Custom URL Schemes Are Still Useful<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Despite the limitations, custom URL schemes have valid use cases:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">App-to-App Communication<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your company has multiple apps, custom URL schemes provide a simple way for them to communicate:<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ From App A, open App B to a specific screen\nif let url = URL(string: &quot;otherapp:\/\/settings\/notifications&quot;) {\n    UIApplication.shared.open(url)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is safe because both apps are under your control. The security risk of scheme hijacking is minimal.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Push Notification Deep Links<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Push notifications are handled by the app itself, so the URL does not need to go through a browser or the OS&#39;s Universal Links system:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;aps&quot;: {\n    &quot;alert&quot;: &quot;Your order shipped!&quot;\n  },\n  &quot;deep_link&quot;: &quot;myapp:\/\/orders\/456\/tracking&quot;\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The app receives the notification, extracts the <code>deep_link<\/code> field, and navigates to the tracking screen. No need for AASA or domain verification.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Legacy Support<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Migrating to Universal Links \/ App Links<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The Migration Path<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Implement Universal Links (iOS) and App Links (Android)<\/strong> alongside your existing custom scheme.<\/li>\n<li><strong>Update all new link generation<\/strong> to use HTTPS URLs.<\/li>\n<li><strong>Keep the custom scheme handler<\/strong> for backward compatibility.<\/li>\n<li><strong>Redirect old scheme URLs<\/strong> to the new HTTPS format in your app&#39;s route handler.<\/li>\n<\/ol>\n\n\n\n<pre><code class=\"language-swift\">func handleURL(_ url: URL) {\n    if url.scheme == &quot;myapp&quot; {\n        \/\/ Convert custom scheme to Universal Link format\n        let httpsUrl = URL(string: &quot;https:\/\/app.example.com\/\\(url.host ?? &quot;&quot;)\\(url.path)&quot;)!\n        router.handle(httpsUrl)\n    } else {\n        \/\/ Already a Universal Link\n        router.handle(url)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">What to Migrate First<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Priority<\/th>\n<th>Action<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>High<\/td>\n<td>Stop generating custom scheme URLs in email campaigns<\/td>\n<\/tr>\n<tr>\n<td>High<\/td>\n<td>Switch social media share links to HTTPS<\/td>\n<\/tr>\n<tr>\n<td>Medium<\/td>\n<td>Update SDK\/API to return HTTPS deep links<\/td>\n<\/tr>\n<tr>\n<td>Low<\/td>\n<td>Update push notification payloads (custom schemes work fine here)<\/td>\n<\/tr>\n<tr>\n<td>Do not<\/td>\n<td>Remove custom scheme registration (breaks backward compatibility)<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Custom Scheme Best Practices<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you must use custom URL schemes:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Use a unique, specific scheme.<\/strong> <code>mycompanyappname:\/\/<\/code> is better than <code>myapp:\/\/<\/code> because it is less likely to conflict.<\/li>\n<li><strong>Validate all input.<\/strong> Treat the URL as untrusted input. A malicious app could open <code>myapp:\/\/delete-account<\/code> if you are not careful.<\/li>\n<li><strong>Do not put sensitive data in the URL.<\/strong> Tokens, passwords, or API keys in a custom scheme URL can be intercepted by other apps.<\/li>\n<li><strong>Always implement a fallback.<\/strong> Check <code>UIApplication.shared.canOpenURL()<\/code> before opening a custom scheme to avoid silent failures.<\/li>\n<\/ol>\n\n\n\n<pre><code class=\"language-swift\">let url = URL(string: &quot;otherapp:\/\/action&quot;)!\nif UIApplication.shared.canOpenURL(url) {\n    UIApplication.shared.open(url)\n} else {\n    \/\/ Fallback: open the App Store or a web page\n    UIApplication.shared.open(URL(string: &quot;https:\/\/apps.apple.com\/app\/otherapp\/id123456&quot;)!)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note: on iOS, you must declare the schemes you want to query in your <code>Info.plist<\/code> under <code>LSApplicationQueriesSchemes<\/code>, limited to 50 entries (<a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/information_property_list\/lsapplicationqueriesschemes\" rel=\"nofollow noopener\" target=\"_blank\">Apple documentation<\/a>).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Deep Linking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> 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 <a href=\"https:\/\/tolinku.com\/docs\/concepts\/deep-linking\/\">deep linking concepts<\/a> for how Tolinku handles link resolution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the comparison, see <a href=\"https:\/\/tolinku.com\/blog\/uri-schemes-vs-universal-links\/\">URI schemes vs Universal Links: which should you use?<\/a>. For the technical overview, see <a href=\"https:\/\/tolinku.com\/blog\/how-deep-linking-works\/\">how deep linking works: a technical overview<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to register and use custom URL schemes for iOS and Android deep linking. Understand the limitations and when to avoid them.<\/p>\n","protected":false},"author":2,"featured_media":1832,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Custom URL Schemes for Mobile Apps: Setup and Pitfalls","rank_math_description":"Learn how to register and use custom URL schemes for iOS and Android deep linking. Understand the limitations and when to avoid them.","rank_math_focus_keyword":"custom URL schemes","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-custom-url-schemes-guide.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-custom-url-schemes-guide.png","footnotes":""},"categories":[11],"tags":[25,23,20,24,52,69,22,412],"class_list":["post-1833","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-deep-linking","tag-android","tag-app-links","tag-deep-linking","tag-ios","tag-migration","tag-mobile-development","tag-universal-links","tag-url-schemes"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1833","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1833"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1833\/revisions"}],"predecessor-version":[{"id":1834,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1833\/revisions\/1834"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1832"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1833"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1833"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1833"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}