Capacitor
Open your Capacitor app from its links, resolve each link with the API, and find the link that led to an install.
This guide uses @capacitor/app for incoming links and @capgo/capacitor-install-referrer for the Android install referrer. Replace demo with your app's name and YOUR_API_KEY with its key: the sandbox key in debug builds and the live key in release builds.
npm install @capacitor/app @capgo/capacitor-install-referrer
npx cap sync1. Set up iOS and Android
iOS: open ios/App/App.xcodeproj in Xcode (ios/App/App.xcworkspace in projects created before Capacitor 8, which use CocoaPods), select the App target, open Signing & Capabilities, add Associated Domains and enter applinks:demo.dev.thedbl.ink, plus one entry per custom domain. If your app has a URL scheme, add it under Info › URL Types too.
Android: in android/app/src/main/AndroidManifest.xml, add a verified intent filter to the main activity, plus a filter for your URL scheme if your app has one:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="demo.dev.thedbl.ink" />
</intent-filter>
<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>Your app's settings must have the iOS team and bundle IDs and the Android package and signing fingerprints the builds use.
2. Resolve the link that opened the app
Your web code calls the API with fetch; the API accepts requests from any origin, including your app's WebView.
import { App } from '@capacitor/app'
const API_KEY = 'YOUR_API_KEY'
export type ResolvedLink = { id: number; longUrl: string; shortUrl: string; isSandbox?: boolean }
export async function post(path: string, body: unknown): Promise<ResolvedLink | null> {
const response = await fetch(`https://dev.thedbl.ink${path}`, {
method: 'POST',
headers: { 'Api-Key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (response.status === 404) return null
if (!response.ok) throw new Error(`Deep Blue Links answered ${response.status}`)
return response.json()
}
export const resolveLink = (url: string) => post('/api/links/resolve', { url })
let lastUrl: string | undefined
async function openUrl(url: string) {
if (url === lastUrl) return
lastUrl = url
const opened = new URL(url)
if (opened.protocol === 'myapp:') {
router.open('/' + opened.host + opened.pathname + opened.search)
return
}
let link: ResolvedLink | null = null
try {
link = await resolveLink(url)
} catch (error) {
console.warn(`Couldn't resolve ${url}`, error)
}
const destination = new URL(link?.longUrl ?? url)
router.open(destination.pathname + destination.search)
}
export function listenForLinks() {
App.addListener('appUrlOpen', ({ url }) => openUrl(url))
App.getLaunchUrl().then((launch) => { if (launch?.url) openUrl(launch.url) })
}Call listenForLinks() when your app starts. Both calls can report the URL that launched the app, so openUrl skips a URL it just handled. router stands for your app's navigation. A 404 from resolve means the URL isn't one of your short links, such as a link's full URL, so it's routed as it is, and so is a URL whose resolve fails, such as with no network. The myapp: branch handles your URL scheme, whose host is the link's first path segment. When link.isSandbox is true, load the destination's content from your sandbox backend.
3. Find the link behind an install
On Android, read the short link from the Play install referrer. Elsewhere, call check-deferred with the timezone and the screen in physical pixels, measured the same way as the preview page measures it:
import { Capacitor } from '@capacitor/core'
import { InstallReferrer } from '@capgo/capacitor-install-referrer'
export async function findInstallLink(): Promise<ResolvedLink | null> {
if (Capacitor.getPlatform() === 'android') {
const result = await InstallReferrer.getReferrer()
const link = new URLSearchParams(result.referrer ?? '').get('link')
return link ? resolveLink(link) : null
}
return post('/api/links/check-deferred', {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
device: {
screenWidth: Math.round(screen.width * devicePixelRatio),
screenHeight: Math.round(screen.height * devicePixelRatio),
language: navigator.language,
},
})
}Call findInstallLink() once, on the first launch (store a flag, for example in localStorage or with @capacitor/preferences), and route like a resolved link when it returns one. A null result is normal: most launches don't follow a link.
Test it
- iOS universal links work on a device: paste a short link into Notes and tap it.
- On Android, check verification with
adb shell pm get-app-links com.example.demo, then open a link withadb shell am start -a android.intent.action.VIEW -d "https://demo.dev.thedbl.ink/Xy3kP9aQ". - The install referrer is only present for installs from Google Play, such as an internal testing track opened from a link's preview page.