Expo deep-link integration guide - lynkily
Deep Links

Deep links in an Expo app — A→Z

Wire a lynkily link to your React Native / Expo app and test it in local dev. Follow it top to bottom; each step says what it’s for and how to check it worked.

The whole thing in one breath

Put the same URL scheme (e.g. acme) in two places — your lynkily app settings and your Expo app.json — build a dev client (Expo Go can’t test custom schemes), then share a lynkily short link bound to the app. Tapping it opens your app; if it’s not installed it goes to the store. Routing to a specific screen and install attribution are optional add-ons.

Step 1

Prerequisites

  • An Expo project on SDK 50+ (config plugins / expo run assumed).
  • Your app’s iOS bundle identifier and Android package (e.g. com.acme.app). In Expo these live in app.json — see step 3.
  • A URL scheme you choose — a short lowercase word, e.g. acme. You’ll reuse it everywhere. It opens your app at acme://.
  • A device or simulator: iOS needs macOS + Xcode; Android needs Android Studio / an emulator or a USB device with adb.

Decide your scheme now (we’ll use acme) and keep it identical in every step. A mismatch here is the #1 reason links don’t open.

Step 2

Register the app in lynkily

This tells lynkily how to open and how to fall back per platform.

  1. Go to Deep Links → Apps → Register an app.
  2. Turn on iOS and/or Android and fill each field (every field has a “where to find this” hint):
    • iOS: Bundle ID com.acme.app, Apple Team ID, App Store ID (once published), URL scheme acme — no ://.
    • Android: Package com.acme.app, URL scheme acme. (SHA-256 is only needed later for silent App Links.)
  3. Leave Universal / App-link domain = None for now (no website required), and Save.

The app’s setup page must say “Ready to deep link.” If it says “iOS is on but has no URL scheme,” you left the scheme blank — fill it and save.

Step 3

Configure Expo — app.json

The scheme is what makes acme:// open your app. It must match the scheme you typed in lynkily. Bundle ID / package must match too.

// app.json  (or app.config.js → export default { expo: { … } })
{
  "expo": {
    "scheme": "acme",
    "ios":     { "bundleIdentifier": "com.acme.app" },
    "android": { "package": "com.acme.app" }
  }
}

Multiple schemes? "scheme": ["acme", "com.acme.app"] is allowed — lynkily just needs one of them.

Step 4

Build a development build — Expo Go can’t test this

Important: Expo Go registers its own scheme (exp://), so your custom acme:// and Universal Links do not work inside Expo Go. You need a development build (your own native app with the dev menu). This is a one-time setup.

# add the dev client, then build & run on a simulator/device
npx expo install expo-dev-client

npx expo run:ios        # builds the native iOS app locally (macOS + Xcode)
npx expo run:android    # builds the native Android app locally

Prefer the cloud? eas build --profile development gives you an installable dev build instead. After this you run your JS with npx expo start --dev-client.

You now have an installed app named after your app (not “Expo Go”). Every time you change scheme, associatedDomains, or intent filters, you must rebuild (steps above) — those are native changes, not JS.

Step 5 · optional

Handle the path / route to a screen

Skip this if you only want to open the home screen — the scheme alone does that. Add it when you want a link to land on a specific screen.

Using expo-router? It handles deep links for you: an incoming path like /product/123 maps straight to app/product/[id].tsx. Nothing else to do.

Not using expo-router? Read the URL with expo-linking:

// App.tsx
import * as Linking from 'expo-linking';
import { useEffect } from 'react';

export default function App() {
  const url = Linking.useURL();               // e.g. "acme://product/123"
  useEffect(() => {
    if (!url) return;
    const { hostname, path, queryParams } = Linking.parse(url);
    // route to the screen for `path` here
  }, [url]);
  // …
}
Step 6

Create the lynkily link you’ll share

You never share a raw acme:// URL. You share a lynkily short link bound to the app — it picks the scheme, platform and store fallback automatically.

  1. Create a link → in “Open your app” choose your app.
  2. Leave “In-app path” empty → opens the home screen. (Set /product/123 to land on a screen — pairs with step 5.)
  3. Save → you get https://lynkily.com/aBc123. That’s the link you share (bio, ads, email, QR).
Step 7

Test it in local dev

First prove the scheme works (rules out lynkily), then prove the lynkily link works end-to-end. Do this against your dev build from step 4.

iOS

# Simulator — open the scheme directly (should open your app on home)
xcrun simctl openurl booted "acme://"

# A specific path
xcrun simctl openurl booted "acme://product/123"
  • Physical iPhone: type acme:// into the Notes app and tap it (Safari’s address bar treats it as a search — don’t use it).

Android

# Emulator or USB device (adb) — open the scheme
adb shell am start -a android.intent.action.VIEW -d "acme://" com.acme.app

# A specific path
adb shell am start -a android.intent.action.VIEW -d "acme://product/123" com.acme.app

Then the real thing — the lynkily link

  • Open https://lynkily.com/<code> on the device (message it to yourself, or scan its QR).
  • Installed → your app opens. Not installed → the App Store / Play Store. Desktop → your web fallback.

Scheme test opens the app but the lynkily link doesn’t? Re-check that the scheme in lynkily exactly equals app.json’s scheme, and that the app’s setup page says “Ready to deep link.”

Step 8 · optional

Universal / App Links — silent, no browser bounce

The scheme path works everywhere but can flash a browser first. For a fully silent open, use https links verified against a domain. This needs a verified branded domain in lynkily (which then hosts the Apple/Android association files for you), plus native config + a rebuild.

// app.json — replace links.acme.com with your verified lynkily domain
{
  "expo": {
    "ios": {
      "associatedDomains": ["applinks:links.acme.com"]
    },
    "android": {
      "intentFilters": [{
        "action": "VIEW",
        "autoVerify": true,
        "data": [{ "scheme": "https", "host": "links.acme.com" }],
        "category": ["BROWSABLE", "DEFAULT"]
      }]
    }
  }
}
  • Add your app’s SHA-256 signing fingerprint in the lynkily app settings (Play Console → App integrity), so Android can verify the link.
  • Rebuild (step 4). Confirm the hosted files return JSON — the setup page links them (/.well-known/apple-app-site-association and /.well-known/assetlinks.json).
  • Android check: adb shell pm get-app-links com.acme.app → you want verified. iOS caches the AASA — delete + reinstall to refresh after changes.
Step 9 · optional

Install attribution + events

Call this once on first launch to recover the screen the user was headed to and the campaign / channel / ad that drove the install — even if they installed from the store first (deferred deep linking).

// first launch — deferred deep link + attributed install
const res = await fetch(
  "https://lynkily.com/api/v1/attribution/init/",
  { method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ app: "your-app-slug", platform: "ios" }) }
);
const data = await res.json();
// → { matched: true, install_id, deep_link: { path: "/product/123" },
//     attribution: { campaign, channel, click_id, gclid } }
// use install_id on later events; route to deep_link.path if present
// later — report conversions/revenue so they attribute back to the link
await fetch("https://lynkily.com/api/v1/events/",
  { method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ app: "your-app-slug", name: "purchase",
      value: "9.99", currency: "USD", install_id }) });

Your app slug is shown on the app’s setup page. The events response includes the iOS SKAdNetwork conversion value to set.

Step 10

Troubleshooting

Link opens the browser, not the appScheme not registered in the installed build. Confirm scheme in app.json, then rebuild (step 4) — a JS reload isn’t enough. Test acme:// in Notes / via adb first.
“Deep linking isn’t active yet” in lynkilyYou enabled a platform but left its URL scheme blank. Edit the app, fill the scheme, save.
Works in Notes but not from the lynkily linkThe scheme in lynkily ≠ the one in app.json. Make them identical (no ://).
Nothing works in Expo GoExpected — Expo Go can’t use custom schemes or Universal Links. Use a dev build (step 4).
Universal (https) link still bounces through SafariAASA not picked up yet: verify the file returns JSON, delete + reinstall the app (iOS caches it), and rebuild after any associatedDomains change.
Android https link opens a chooser / browserApp Links not verified. Add the SHA-256 in lynkily, set autoVerify: true, rebuild, and check adb shell pm get-app-links shows verified.
No install / attribution in analyticsThe app isn’t calling attribution/init on first launch (step 9). The click itself should still show immediately.

Still stuck? Contact support with your bundle ID / package and what you see.