Skip to content
Bugwise
Menu
Features & Flows

Deep Links

How links from emails, SMS, QR codes, ads and push notifications open the right screen in an app, what usually breaks, and how to trigger and test deep links on Android and iOS.

40 min intermediate Mobile & web

Your friend sends you a link to a rain jacket. You tap it, and instead of the jacket, the shopping app opens on its Home screen. Or it opens the website and asks you to log in again. Or it opens the right product, but pressing Back throws you out of the app. You shrug and close it. The marketing team paid for that click, and nobody will ever know why it didn’t turn into a sale.

Links into apps are everywhere: newsletters, order emails, SMS, QR codes on posters, ads, Instagram posts, push notifications. They connect the outside world to one exact screen in the app, and they cross many systems on the way: the email tool, the browser, the operating system, the website and the app. That’s exactly where bugs hide. By the end of this lesson you’ll know how deep links work, which situations to test, and how to fire links at a phone in seconds.

How it works

A deep linkDeep linkA link that opens a specific screen inside an app instead of just its home screen.Open in glossary → is a link that opens a specific screen inside an app, not just the app’s start screen. “Open Pinecone” is a normal link. “Open Pinecone on product 42” is a deep link.

  1. Someone taps a link. In an email, an SMS, a chat, a QR code scanned with the camera, an ad, or a notification.
  2. The phone decides who opens it. Is there an installed app that’s allowed to handle this link? If yes, the app opens. If not, the browser (or the app store) opens instead.
  3. The app reads the link. It takes the path and parameters (for example /product/42) and works out which screen that means. This part of the app is called the router.
  4. The app checks the situation. Is the user logged in? Does product 42 still exist? Is the user allowed to see order 8812?
  5. The app shows the screen, with a sensible history. Product 42 is on screen, and Back leads somewhere that makes sense (usually the app’s Home), not out of the app.

There are two ways to build a deep link, and testers meet both.

Custom URL schemeUniversal Link (iOS) / App Link (Android)
Looks likepinecone://product/42https://pinecone.example/product/42
App not installedNothing happens or an errorThe website opens (a free web fallback)
Who can claim itAny app can register pinecone://Only the app the website owner approves
Typical usePush notifications, links between your own appsEmails, SMS, QR codes, ads, social posts

A custom scheme is like a nickname that the app registers on the phone: “send me everything that starts with pinecone://”. It’s simple, but it has two problems: if the app isn’t installed, the link is dead, and another app could register the same nickname.

Universal Links (Apple’s name) and App Links (Android’s name) are normal https:// web addresses. If the app is installed, the phone opens the app. If not, the same link simply opens the website. To stop any random app from grabbing pinecone.example links, the website has to vouch for the app with a small verification file:

The file is called apple-app-site-association (no .json at the end) and lives in the /.well-known/ folder of the website. It lists which app may open which paths:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.pinecone.app"],
        "components": [{ "/": "/product/*" }, { "/": "/orders/*" }, { "/": "/go/*" }]
      }
    ]
  }
}

The app itself also lists the domains it wants (applinks:pinecone.example) in its settings, called Associated Domains. Apple downloads the file through its own servers when the app is installed or updated, so a fixed file can take a while to reach phones.

The file is called assetlinks.json, lives in the /.well-known/ folder of the website, and names the app’s package and the fingerprint of the certificate it was signed with:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.pinecone.app",
    "sha256_cert_fingerprints": ["14:6D:E9:83:...:A5"]
  }
}]

Android checks the file when the app is installed. If the check fails, Android 12 and newer open the link in the browser instead of the app.

Dev note
The app’s manifest lists the domains with android:autoVerify="true".

A normal link can’t survive an app install. You tap an ad for “10% off your first tent”, go to the store, install, open the app… and land on a generic welcome screen. The offer is gone.

A deferred deep link fixes this. The link first goes through a link service that remembers what you wanted. After installation, the app asks “was this person sent here by a link?” and opens the offer on first launch. Android has a built-in helper for this (the Play Install Referrer). iOS has no built-in equivalent, so link services (Branch, AppsFlyer, Adjust and similar) use their own matching, which is not 100% reliable. Firebase Dynamic Links, which many older apps used, was shut down in 2025, so old links that still point to it are worth a check.

The tricky part for testers: the offer must open exactly once. Not zero times, and not on every launch. See also Onboarding, which usually runs right before.

Try it: what happens when you tap?

Pick a link type, whether the app is installed, and where the link was tapped:

Link type
App on the phone
Tapped in

https://shop.example/product/42

  1. At install time, the phone downloaded the verification file from shop.example and confirmed the app may open its links.
  2. The phone opens the app directly, no browser in between.

Works: Product 42 opens in the app.

Tester’s note: If the verification file is missing, wrong, or not on every domain (www!), the link silently opens the website instead.

The same link can behave differently depending on where it’s tapped. A good test plan covers the real sources:

SourceWhy it’s special
EmailMarketing tools often wrap links in a click-tracking redirect (click.mailer.example/…), so the phone sees their domain, not yours, and opens the browser
SMS / chat appsUsually work well, but some messengers open links in their own in-app browser
Push notificationsOften use a custom scheme or an ID. See Push Notifications
QR codesScanned with the camera app; long URLs make dense codes that are hard to scan from a poster
Ads and social appsInstagram, Facebook, TikTok and LinkedIn often open links in their in-app browser, which shows the website instead of opening your app
Web pagesA link from your own website to your own domain usually stays in the browser on iOS. Tapping from another site opens the app

Example requirements

Criteria 1 (both domains), 3 (parameters), 4 (continue after login) and 6 (Back) are the ones developers most often forget, because nobody writes them down. If your story doesn’t mention them, ask.

What usually goes wrong

#BugWhat you seeTypical severity
1Link opens the website instead of the appVerification file missing on one domain (often www), wrong fingerprint, or wrapped in click trackingHigh
2Target lost after loginLogged-out user taps an order link, logs in, lands on HomeMedium–High
3Deleted or old content crashesOld email link to a removed product: crash, blank screen or endless spinnerHigh
4Back exits the appApp started from a link has only one screen in its historyMedium
5Parameters break routing?utm_source=email makes the app show “Page not found”High (marketing links all carry them)
6Works only when the app is already openFrom a killed state the link opens Home and the target is ignoredHigh
7Deferred link lost or repeatedAfter install the offer never opens, or opens on every launchMedium
8Wrong user’s dataLink to /orders/8812 shows another customer’s orderCritical (security)
9Custom scheme used where the app may be missingpinecone:// in an email does nothing for people without the appMedium
10No web fallbackThe https link opens a 404 page on the websiteHigh

Find the bug

Pinecone, an outdoor gear shop, has six deep-link bugs. On the left are links as they would arrive by email, SMS, QR code, push and ads. Change the device state between tests like a real tester would:

  • Tap every link with the app killed, then again with it in the background (open the app and press Home).
  • Log out, then try the order links.
  • Uninstall the app and tap the ad.
  • After every link, press Back and see where you end up.
  • Watch the device log.

🐞 Bug hunt: Pinecone deep links

This demo has 6 hidden bugs. Explore it like a tester, then check your findings.

Found0/6

Device state

Pinecone installed
Logged in
App: Not running

Incoming links: tap one to open it on the phone

Device log

  1. Nothing yet. Tap a link.
9:41

9:41

Wednesday, 15 October

  1. 1

    Hint: Tap the SMS from Jonas with the app installed. Then compare with the QR code link.

  2. 2

    Hint: Turn "Logged in" off and tap the "order has shipped" email or the push.

  3. 3

    Hint: Tap the old 2024 email.

  4. 4

    Hint: With the app not running, tap the QR code link and press Back. Then open Pinecone from the home screen, press Home, tap the same link and press Back.

  5. 5

    Hint: Compare the newsletter link with the QR code link. Both point to product 42.

  6. 6

    Hint: Turn "Pinecone installed" off, tap the banner ad, install and open the app. Finish onboarding, then reopen the app from the home screen.

Test scenarios

Deep links multiply quickly: every link × every app state × logged in or out. Use the scenarios below as a menu, and agree with your team which combinations matter most.

Functional

  • Every supported link type opens the right screen with the right content: products, categories, orders, search results, campaigns, password reset (Password Reset).
  • App states: open in the foreground, in the background, and not running (killed). The killed case is a separate code path (a cold start) and breaks most often. Read Background and Foreground for how to kill an app properly and why the OS does it on its own.
  • Logged in vs logged out: links to private screens ask for login and then continue to the target. Links to public screens don’t ask for login at all.
  • Installed vs not installed: https links fall back to the website. Campaign links survive the install (deferred deep links) and open once.
  • Back and Up: after the link, Back goes to a sensible place. Test from a cold start and from the background.
  • Tabs: a link to an order selects the Orders tab (or wherever orders live), so the bottom navigation matches the screen.
  • Both domains: pinecone.example and www.pinecone.example, and any short-link or tracking domain the marketing team uses.

Negative

  • Product, order or campaign ID that doesn’t exist or was deleted.
  • IDs in the wrong format: /product/abc, /product/-1, /product/ with nothing after it.
  • Unknown paths: /sale2023, /product/42/reviews if reviews have no deep link.
  • An expired campaign or a password reset link that was already used.
  • A link to an order that belongs to a different user (log in as user B and open user A’s order link).
  • The app is installed but outdated and doesn’t know a new link type yet.

Edge cases

  • Query parametersQuery parameterData in a URL after the ?, like ?q=mug&sort=price, often used to keep search and filter state.Open in glossary →: ?utm_source=email (often with utm_medium and utm_campaign too), ?ref=ad, parameters in a different order, unknown parameters, a #fragment at the end.
  • Upper case and trailing slashes: /Product/42, /product/42/.
  • Special characters: search links like /search?q=rain%20jacket or q=Zoë.
  • Tapping a link while a modal, a payment sheet or an unsaved form is open. Does the app warn before throwing away the user’s input?
  • Two links quickly one after another. The second one should win, without a stack of duplicate screens.
  • A link that arrives during onboarding, the login screen, or a forced update screen.
  • Deep link while logged in as a different user than the one the email was sent to.

UI & usability

  • The target screen shows a loading state while it fetches data, not an empty screen.
  • “No longer available” screens offer a way forward (similar products, Home).
  • Web fallback pages work on a phone and show a clear “Open in app” / “Get the app” banner.
  • iOS shows a small ”◀ Mail” (or ”◀ Messages”) button in the status bar after a link. Tapping it should return the user to where they came from.
  • The link preview (title and image) looks right when the link is pasted into a chat app.

Security

Accessibility

You don’t need to send yourself an email for every test. Each platform can fire a link at a phone directly.

With ADB and a phone or emulator connected:

# Open a link as if it was tapped (Android decides: app or browser)
adb shell am start -W -a android.intent.action.VIEW -d "https://pinecone.example/product/42"

# Force it into a specific app (skips the verification check, so don't use it to test verification)
adb shell am start -W -a android.intent.action.VIEW -d "pinecone://orders/8812" com.pinecone.app

# Links with & need quotes around the whole command, or the device shell cuts them off
adb shell 'am start -W -a android.intent.action.VIEW -d "https://pinecone.example/product/42?utm_source=email&utm_medium=newsletter"'

# Android 12+: is App Link verification OK for each domain?
adb shell pm get-app-links com.pinecone.app

# Kill the app first to test a cold start
adb shell am force-stop com.pinecone.app

pm get-app-links lists every domain with a state such as verified, or a failure. Ask the developers to re-run verification with adb shell pm verify-app-links --re-verify com.pinecone.app after a fix. In Android Studio, Tools → App Links Assistant can also test links and check the assetlinks.json file.

In the iOS Simulator:

# Open a link in the booted simulator
xcrun simctl openurl booted "https://pinecone.example/product/42"
xcrun simctl openurl booted "pinecone://orders/8812"

# Kill the app first to test a cold start
xcrun simctl terminate booted com.pinecone.app

On a real iPhone, paste links into a note in the Notes app or send them to yourself in Messages, then tap them. A few iOS habits to know:

  • Typing a Universal Link into Safari’s address bar opens the website, not the app. Only tapping a link opens the app.
  • If you tap the domain name in the top-right corner after the app opened, iOS remembers that you prefer the browser for that domain. Long-press a link and pick Open in “Pinecone” to switch back.
  • Universal Link files are fetched (through Apple’s servers) when the app is installed. After the team fixes the file, reinstall the app, and allow some time: Apple may keep serving the old file for a while (up to about a day).
  • QR codes: paste the link into any QR code generator on your computer and scan the screen with the phone’s camera. Great for testing real posters and packaging links, including long URLs.
  • Chat and email: send links to yourself through the channels customers use: email (in the Gmail and Outlook apps, not only the built-in mail app), SMS, WhatsApp.
  • Social apps: post or DM the link in Instagram, Facebook or LinkedIn (a test account) and tap it there to see the in-app browser.
  • A test page: many teams keep a simple internal web page with all supported links as tappable buttons. Open it on the phone and tap away. Ask if your team has one.
  • Push: send a test notification with the deep link inside, see Push Notifications.

Example test cases

TC-DL-001Product link opens the app from a cold start

Preconditions

  • App installed, user logged in
  • App force-stopped

Steps

  1. Open pinecone.example/product/42 from Messages
  2. Check the screen
  3. Press Back

Expected result

The app opens directly on Trail Tent 2P (no browser in between). Back shows the app Home screen.

TC-DL-002www and bare domain both open the app

Preconditions

  • App installed

Steps

  1. Tap pinecone.example/product/17
  2. Return to Messages
  3. Tap www.pinecone.example/product/17

Expected result

Both links open the Storm Rain Jacket screen in the app.

TC-DL-003Logged-out user continues to the order after login

Preconditions

  • App installed, user logged out
  • Order 8812 belongs to sam@test.com

Steps

  1. Tap pinecone.example/orders/8812
  2. Log in as sam@test.com

Test data

sam@test.com / Test1234!

Expected result

The Log in screen appears first. After login, order #8812 opens. Back goes to Home.

TC-DL-004Tracking parameters do not break routing

Preconditions

  • App installed

Steps

  1. Open pinecone.example/product/42 with the parameters ?utm_source=newsletter&utm_medium=email&utm_campaign=spring added

Expected result

Trail Tent 2P opens. The campaign parameters appear in the analytics event for the app open.

TC-DL-005Link to a deleted product

Preconditions

  • Product 999 was removed from the catalog

Steps

  1. Open pinecone.example/product/999 with the app killed
  2. Repeat with the app in the background

Expected result

Both times the app shows "This product is no longer available" with a link to similar products. No crash, no blank screen.

TC-DL-006Web fallback when the app is not installed

Preconditions

  • App not installed

Steps

  1. Tap pinecone.example/product/42 in an email

Expected result

The product opens on the website in the browser, with a "Get the app" banner. No error page.

TC-DL-007Deferred campaign link opens once after install

Preconditions

  • App not installed
  • Clean test device

Steps

  1. Tap pinecone.example/go/spring-tent
  2. Install the app from the store and open it
  3. Finish onboarding
  4. Kill the app and open it again from the home screen

Expected result

After onboarding, the tent offer opens. On the second launch the normal Home screen opens and onboarding is not shown again.

TC-DL-008Order link of another user is not shown

Preconditions

  • Logged in as lea@test.com
  • Order 8812 belongs to sam@test.com

Steps

  1. Open pinecone.example/orders/8812

Expected result

The app shows "Order not found" (or similar). None of Sam's order details are shown, and the API returns 403 or 404.

Tip: click a test case to expand it.

Mobile vs web

  • Two link systems: Universal Links on iOS and App Links on Android behave differently. Test both platforms, they rarely break the same way.
  • App states matter a lot: foreground, background and killed each take a different path through the code. See Background and Foreground.
  • The back stack is the app’s job. After a cold start there’s no history, so the app has to build one.
  • Install and update: verification files are read at install time. Test fresh installs, updates from an older version, and links to screens an old version doesn’t know.
  • Notifications usually carry deep links too. Test them together with Push Notifications.
  • On the web, every page already has a URL, so “deep linking” mostly means: every URL works when opened directly, not only when you click your way there. Paste a product URL into a fresh private window.
  • Single-page apps can break this: the page works when you navigate, but reloading or opening the URL directly shows a 404. Test refresh on every important page.
  • Logged-out users should go through login and come back to the page (often a ?redirect= or returnUrl parameter). Check that the redirect only goes to your own site, not to any URL in the parameter (an “open redirect”).
  • The website is also the fallback for the app’s links. Every path the app supports needs a working page here.
  • Check the “Get the app” / “Open in app” banner: it shouldn’t cover content, should be dismissible, and should remember being dismissed.

Checklist

Deep links checklist

0 of 13 checked

Check your understanding

Quick quiz

0/6 answered · 0 correct

  1. 1.Which link opens the website when the app is NOT installed?

  2. 2.Links to pinecone.example open the app, but links to www.pinecone.example open the browser. What is the most likely cause?

  3. 3.A logged-out user taps an order link, logs in, and lands on Home. What is the bug?

  4. 4.You open a product link with the app killed and press Back. The app closes. Why does this matter?

  5. 5.Which adb command opens a link on a connected Android phone as if it was tapped?

  6. 6.A user taps a link in an Instagram post and sees your website inside Instagram, even though your app is installed. What should you check?

Key takeaways

  • A deep linkDeep linkA link that opens a specific screen inside an app instead of just its home screen.Open in glossary → opens one specific screen in an app. Custom schemes (pinecone://) are simple but die without the app; Universal Links / App Links are https links that fall back to the website.
  • Universal Links and App Links need a verification file on every domain (apple-app-site-association, assetlinks.json). A missing www file or a wrong certificate fingerprint quietly sends users to the browser.
  • Test every important link × installed or not × logged in or out × foreground, background or killed.
  • Classic bugs: target lost after login, deleted IDs crashing, Back exiting the app, utm parameters breaking routes, deferred links lost or repeated, and in-app browsers that never open the app.
  • Fire links quickly with adb shell am start -a android.intent.action.VIEW -d "…", xcrun simctl openurl booted "…", Notes or Messages, and QR codes, and reset the device state between runs.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its review cards are now in Bites ⚡
Esc

Type to search the whole site.