# Redirectly > Deferred deep linking and install attribution for Flutter and React Native. > Redirectly is a smart deferred deep linking and install attribution platform for mobile apps, built as a Firebase Dynamic Links replacement. It provides branded short links on {yourapp}.redirectly.app subdomains, native Flutter and React Native SDKs, device-fingerprint install attribution, click analytics, and universal-link (AASA / assetlinks.json) hosting. Users who tap a link before installing your app still land on the right screen after their first launch. > Plans from $49/mo with a 14-day free trial (no card required). Every page listed below has a markdown version: append `.md` to its URL. Structured product data (JSON): https://redirectly.app/api/ai Developer docs: https://docs.redirectly.app Dashboard / signup: https://dashboard.redirectly.app --- # See which campaigns actually drive installs > Redirectly is deep linking + install attribution for mobile apps. See which campaigns drive installs — track clicks, installs, and conversions by source. - Canonical: https://redirectly.app/ - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Deep linking & install attribution for mobile apps. Firebase Dynamic Links shut down Aug 2025 — Redirectly is deep linking + install attribution in one dashboard. See every click, install, and conversion — by campaign, channel, device, and country. Drop in one SDK and you're live in 30 minutes. 14-day free trial, no card required. Plans from $49/mo. Trusted by teams shipping real apps: Drive Connexa, Pension Plus, Waktoo, Omda, Pappad, QCEC. ## Every install, traced back to the campaign that earned it. Clicks, installs, and conversion in real time — broken down by campaign, channel, device, and country. Organic vs paid, finally clear. No spreadsheets, no overnight batch reports. ## Branch charges $500/mo. We start at $49. Same deferred deep linking. Same attribution. Same SDKs. Without the enterprise sales call. | Platform | Status | Price | Flutter | Setup | Deferred | |---|---|---|---|---|---| | Firebase | Dead | — | None | — | No | | Branch | Active | ~$500 | Community | Days | Yes | | AppsFlyer | Active | ~$800 | Community | Days | Yes | | Redirectly | Active | $49 | Native | 30 min | Yes | ## Your links break at the app store. Ours don't. A complete deep linking platform for mobile apps. No enterprise contract needed. - **Deferred Deep Linking** — Links survive app installation. User clicks → store → installs → lands exactly where they should. - **Platform Routing** — Auto-detect iOS or Android. Route to the right store. Desktop gets a fallback page with QR code. - **Install Attribution** — Know which campaigns, creatives, and channels drove each install. Organic vs. paid. - **Live Dashboard** — Clicks, installs, UTMs, geo data. Real-time, not overnight batch reports. - **Custom Subdomains** — links.redirectly.app — your brand on every deep link. - **Native SDKs** — First-class Flutter on pub.dev. React Native included. Built by us, not community forks. ## SDKs & platform support Redirectly ships native SDKs for both Flutter (`flutter_redirectly`, first-class on pub.dev) and React Native (`react-native-redirectly`) — built by us, not community forks. Both SDKs support deferred deep linking and install attribution on iOS and Android. See the [Flutter SDK guide](https://redirectly.app/flutter-deferred-deep-linking.md), the [React Native SDK guide](https://redirectly.app/react-native-deferred-deep-linking.md), and the full documentation at https://docs.redirectly.app. ## Three steps. Three lines of code. Your engineers wire it up once — under 30 minutes — then your marketing team lives in the dashboard. ### 01 — Install the SDK Add flutter_redirectly to your app. Zero config. Under 50KB. ```bash flutter pub add flutter_redirectly ``` ### 02 — Listen for deep links On app start, listen for incoming links. Works for both new installs and existing users. ```dart final redirectly = FlutterRedirectly(config); redirectly.onLinkClick.listen((click) { navigator.push(click.originalUrl); }); ``` ### 03 — Create links with attribution Generate deep links with UTM params, referral data, or verification tokens. Share anywhere. ```dart final link = await redirectly.createLink( target: "myapp://promo", params: { "utm_source": "facebook", "utm_campaign": "summer_sale", }, ); ``` ## Simple pricing. Everything included. Same SDKs and attribution on every plan — you pay for install volume and team size, not feature unlocks. 14-day free trial, no card required. Annual billing gives 2 months free. - **Starter — $49/mo** (or $490/yr): For solo devs shipping 1–3 apps. 3 projects · 3 team members, 1,000 app installs / mo, deferred deep linking + attribution, 1-year analytics retention. - **Growth — $149/mo** (or $1,490/yr — most popular): For small teams scaling installs. 10 projects · 10 team members, 25,000 app installs / mo, everything in Starter, 2-year analytics retention. - **Scale — $399/mo** (or $3,990/yr): For companies that need headroom. Unlimited projects & members, 250,000 app installs / mo, audit log + priority support, 5-year analytics retention. - **Enterprise — custom pricing** tailored to your volume: Beyond 250k installs / mo. Custom install volume, unlimited projects & members, SSO + SLA, priority support. Contact sales at support@redirectly.app. Every plan — and the 14-day free trial — includes: - Native Flutter & React Native SDKs - HMAC-signed deep-link tokens - AASA / assetlinks generator - Multiple iOS bundles + Android packages per project - Role-based access (owner / admin / editor / viewer) - CSV export — even during the trial Full plan details: [Pricing](https://redirectly.app/pricing.md). ## FAQ ### What is deferred deep linking? Deferred deep linking allows a link to survive the app installation process. If a user clicks your link but doesn't have the app, they are redirected to the App Store. After installing and opening the app, Redirectly retrieves the original link data so you can route the user to the correct content. ### How does install attribution work? Redirectly tracks when users install your app after clicking a deep link. We match installs to the original link click using device fingerprinting, allowing you to distinguish between organic and attributed installs. ### Do you support Flutter and React Native? Yes! We provide native SDKs for both Flutter (flutter_redirectly) and React Native (react-native-redirectly). Both SDKs support deferred deep linking and install attribution on iOS and Android. ### Can I migrate from Firebase Dynamic Links? Yes! Redirectly is designed as a drop-in replacement for Firebase Dynamic Links. Our SDKs and API structure are built to make migration easy. ### How much does Redirectly cost? Plans start at $49/mo (Starter), $149/mo (Growth), and $399/mo (Scale), with a custom Enterprise tier above 250,000 installs/month. Every plan includes the native SDKs, deferred deep linking, install attribution, the analytics dashboard, team roles, and CSV export. There's a 14-day free trial, no card required. ## Related - [Pricing](https://redirectly.app/pricing.md) - [Integrations](https://redirectly.app/integrations.md) - [Blog](https://redirectly.app/blog.md) - [Firebase Migration Guide](https://redirectly.app/firebase-dynamic-links-alternative.md) - [Branch Alternative](https://redirectly.app/branch-alternative.md) - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) - [Flutter SDK](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native SDK](https://redirectly.app/react-native-deferred-deep-linking.md) - [AASA Validator](https://redirectly.app/aasa-validator.md) - [Asset Links Validator](https://redirectly.app/assetlinks-validator.md) - [Privacy](https://redirectly.app/privacy.md) - [Terms](https://redirectly.app/terms.md) - [Documentation](https://docs.redirectly.app) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly Pricing > Redirectly plans: Starter $49/mo, Growth $149/mo, Scale $399/mo, Enterprise custom — every plan starts with a 14-day free trial, no card required. - Canonical: https://redirectly.app/pricing - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native All prices in USD. Annual billing gives 2 months free. Every plan starts with a 14-day free trial — no card required. ## Starter — $49/mo ($490/yr billed annually) For solo devs shipping 1–3 apps - 3 projects · 3 team members - 1,000 app installs / mo - Deferred deep linking + attribution - 1-year analytics retention ## Growth — $149/mo ($1490/yr billed annually) For small teams scaling installs - 10 projects · 10 team members - 25,000 app installs / mo - Everything in Starter - 2-year analytics retention ## Scale — $399/mo ($3990/yr billed annually) For companies that need headroom - Unlimited projects & members - 250,000 app installs / mo - Audit log + priority support - 5-year analytics retention ## Enterprise — Custom pricing — contact sales Beyond 250k installs / mo - Custom install volume - Unlimited projects & members - SSO + SLA - Priority support ## Every plan (and the free trial) includes - Native Flutter & React Native SDKs - HMAC-signed deep-link tokens - AASA / assetlinks generator - Multiple iOS bundles + Android packages per project - Role-based access (owner / admin / editor / viewer) - CSV export — even during the trial Start a free trial at https://dashboard.redirectly.app · Enterprise: support@redirectly.app --- Full site index for AI agents: https://redirectly.app/llms.txt --- # About Redirectly > Learn about Redirectly's mission to build the best Firebase Dynamic Links alternative — reliable, developer-friendly deep linking for mobile apps. - Canonical: https://redirectly.app/about - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native We're building the future of deep linking for mobile developers who need reliable, developer-friendly alternatives to Firebase Dynamic Links. ## Our Mission When Google announced the sunset of Firebase Dynamic Links, millions of mobile developers were left scrambling for alternatives. Most solutions required significant code changes, expensive migrations, or compromised on features. We built Redirectly to bridge that gap - providing a drop-in replacement that works with your existing code while offering better performance, analytics, and pricing. ### What We Believe - **Developer Experience First** — Great tools should make your life easier, not harder. - **Performance Matters** — Every millisecond counts in mobile user experience. - **Community Driven** — We build what developers actually need and want. ## By the Numbers - 1,200+ Developers Migrated - 50M+ Links Served - 99.9% Uptime - <50ms Avg Response Time ## Meet the Team We're a small but passionate team of developers who understand the challenges of building reliable mobile applications. ### Dmytro Virych Founder & CEO Mobile developer with 8+ years of experience building apps that scale. Previously frustrated Firebase Dynamic Links user who decided to build a better solution. ## Our Values These principles guide every decision we make and every feature we build. ### Simplicity Complex problems deserve simple solutions. We believe powerful tools can be easy to use. ### Transparency No hidden fees, no vendor lock-in, no surprises. We're upfront about everything. ### Community We listen to our users and build features that solve real problems. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Linking Docs in Your AI Editor > Access Redirectly docs right in your AI editor. MCP Server for Claude Desktop, Cursor, Windsurf, VS Code, Zed, and Continue.dev. Set up deep links in-editor. - Canonical: https://redirectly.app/mcp - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Access Redirectly documentation directly from Claude Desktop, Cursor, Windsurf, VS Code, Zed, and Continue.dev. Search docs, view examples, and build deep links without leaving your editor. ## What is Redirectly MCP Server? The Model Context Protocol (MCP) is an open standard that lets AI models and editors access external tools and data. Redirectly is the first deep linking platform to offer an MCP Server, enabling developers to search documentation, retrieve code examples, and manage deep links directly within their AI editor. ### Stay in Flow No browser tabs. Search docs and examples without leaving your editor or AI conversation. ### Conversational Context Reference documentation in natural language. AI understands context and suggests relevant guides. ### Vibe Coding Ready Perfect for AI-native development. Get documentation, generate code, and build seamlessly. **MCP stands for Model Context Protocol.** It's an open standard developed by Anthropic that allows AI models and editors to safely access external tools, APIs, and data sources. Think of it as a bridge between your AI editor and Redirectly's documentation—your AI can search, retrieve, and reference docs in real-time. ## Available tools The Redirectly MCP Server provides four core tools for accessing documentation and examples. ### search_redirectly_docs Search documentation by keyword or topic. Returns relevant guides, examples, and best practices. `POST /api/search` ### get_redirectly_doc Retrieve a specific documentation page by ID or path. Get full content, code examples, and related topics. `GET /api/docs/{id}` ### list_redirectly_docs List all available documentation pages with metadata. Includes titles, descriptions, categories, and tags. `GET /api/docs` ### get_redirectly_examples Retrieve code examples for Flutter, React Native, and JavaScript. Filter by framework and feature. `GET /api/examples` ## Editor setup instructions Setup takes just a few minutes. Find your editor below and add the MCP Server configuration. ### Claude Desktop Edit your MCP servers configuration file: **macOS/Linux:** `~/.claude/claude_desktop_config.json` **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "redirectly": { "command": "npx", "args": ["-y", "@anthropic/redirectly-mcp-server"] } } } ``` After adding the config, restart Claude Desktop. You'll see "Redirectly" in the tools menu. ### Cursor Edit your MCP servers configuration: **All platforms:** `~/.cursor/mcp-server-config.json` ```json { "mcpServers": { "redirectly": { "command": "npx", "args": ["-y", "@anthropic/redirectly-mcp-server"] } } } ``` Restart Cursor. Redirectly tools will be available in the Tools panel. ### Windsurf Edit your MCP configuration: **All platforms:** `~/.windsurf/mcp.json` ```json { "mcpServers": { "redirectly": { "command": "npx", "args": ["-y", "@anthropic/redirectly-mcp-server"] } } } ``` Restart Windsurf. Look for Redirectly in the tools dropdown. ### VS Code (with Continue) First, install the Continue extension from the VS Code marketplace. Edit your Continue configuration: **All platforms:** `~/.continue/config.json` ```json { "mcpServers": { "redirectly": { "command": "npx", "args": ["-y", "@anthropic/redirectly-mcp-server"] } } } ``` Restart VS Code. Access Redirectly through the Continue sidebar. ### Zed Edit your Zed settings file: **All platforms:** `~/.config/zed/settings.json` ```json { "assistant": { "enabled": true, "mcpServers": { "redirectly": { "command": "npx", "args": ["-y", "@anthropic/redirectly-mcp-server"] } } } } ``` Restart Zed. Use Ctrl+Shift+K to open the assistant with Redirectly tools. ### Continue.dev Edit your Continue configuration: **All platforms:** `~/.continue/config.json` ```json { "mcpServers": { "redirectly": { "command": "npx", "args": ["-y", "@anthropic/redirectly-mcp-server"] } } } ``` Restart Continue. Redirectly tools are now available in your chat context. ### Need help? If you don't see the MCP Server after restart, try: - Clearing your editor cache and restarting - Running `npm install -g @anthropic/redirectly-mcp-server` to install globally - Checking that Node.js 16+ is installed: `node --version` ## Example prompts Once the MCP Server is set up, you can ask your AI editor any of these questions. The AI will search Redirectly docs and provide tailored answers. - **"Set up deferred deep linking in my Flutter app"** — Get step-by-step setup instructions for the Redirectly Flutter SDK with initialization code. - **"Show me how to configure Universal Links for iOS"** — Retrieve documentation for AASA (Apple App Site Association) setup with examples. - **"Generate a referral deep link with attribution"** — Get code examples and API reference for creating referral links with tracking parameters. - **"Help me migrate from Firebase Dynamic Links"** — Access the full Firebase to Redirectly migration guide with code comparisons. - **"Debug why my Android App Links aren't working"** — Get troubleshooting guides, assetlinks.json validation, and common issues. - **"Show me React Native deep linking examples"** — Retrieve React Native-specific documentation and implementation examples. ## Why this matters ### AI-Native Development Workflow Modern development is becoming AI-assisted. Instead of breaking focus to search docs in a browser, developers now use AI editors as their primary workspace. Having documentation available directly in that context means faster iteration, fewer tab switches, and better productivity. ### Vibe Coding & Flow State "Vibe coding" is writing code with AI assistance in a conversational way. The MCP Server lets you reference documentation naturally within that conversation, maintaining flow state and context without interruption. ### Real-Time Context Your AI editor now has live access to Redirectly documentation. When you ask "how do I set up deep linking?", the AI can retrieve the latest docs, examples, and best practices instantly—no manual copy/paste needed. ### Reduced Cognitive Load Keep your brain in the code. Let the AI handle navigation and context switching. You describe what you want to build, and the AI provides documentation and examples in your current workspace. ### The Future of Developer Tools MCP is the emerging standard for how AI editors will access external tools and data. By offering an MCP Server, Redirectly is making deep linking infrastructure accessible within the AI-native development workflow, not alongside it. ## Frequently asked questions ### What is the Model Context Protocol (MCP)? MCP is an open standard developed by Anthropic that allows AI models and editors to safely access external tools, APIs, and data sources. The Redirectly MCP Server bridges your AI editor and Redirectly documentation, enabling real-time search and retrieval. ### Which AI editors and tools are supported? Redirectly MCP Server supports Claude Desktop, Cursor, Windsurf, VS Code (with Continue extension), Zed, and Continue.dev. Each uses the same installation process with a simple configuration file. ### Is the MCP server free? Yes, completely free. The Redirectly MCP Server provides access to public documentation and tools at no cost. You do not need a Redirectly account to use it for documentation access. ### Does it work with Flutter and React Native? Yes. The MCP Server provides documentation and code examples for both Flutter and React Native. You can search for framework-specific guides, implementation patterns, and best practices. ### How is this different from reading docs in a browser? The MCP Server keeps documentation in your development context. Instead of switching browser tabs, you search and retrieve docs directly within your AI editor. This maintains focus, supports flow state, and makes vibe coding more natural. ### Can I use the MCP Server without creating a Redirectly account? Yes. The MCP Server provides documentation access for free without requiring a Redirectly account. If you want to use Redirectly's API to create and manage links, you'll need a free account. ### Does the MCP Server work offline? No, the MCP Server requires internet connectivity to fetch documentation from Redirectly servers. Ensure your editor has network access. ### How often is documentation updated in the MCP Server? Documentation is updated in real-time as changes are published to Redirectly. You'll always have access to the latest guides, examples, and API documentation. ## Related - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native Deep Linking Guide](https://redirectly.app/react-native-deferred-deep-linking.md) - [Redirectly Home](https://redirectly.app/index.md) - [Deep Linking Fundamentals](https://redirectly.app/deferred-deep-linking.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Linking Code Snippets > Copy-paste code snippets for deep linking in Flutter and React Native: email verification, referrals, push notifications, and attribution tracking. - Canonical: https://redirectly.app/snippets - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Copy-paste ready code for Flutter and React Native. Implement email verification, referrals, push notifications, and attribution tracking in minutes. ## Flutter ### Authentication #### Email Verification Deep Link Handler Handle email verification links with token parsing ```dart import 'package:redirectly_sdk/redirectly_sdk.dart'; import 'package:uni_links/uni_links.dart'; class EmailVerificationHandler { Future handleEmailVerification() async { try { // Listen to deep links deepLinkStream.listen((String? link) async { if (link != null && link.contains('/verify')) { final uri = Uri.parse(link); final token = uri.queryParameters['token']; if (token != null) { // Send verification request final response = await verifyEmail(token); if (response.success) { // Navigate to success screen navigateTo('/email-verified'); } } } }); } catch (e) { print('Email verification error: $e'); } } Future verifyEmail(String token) async { // Your verification logic return VerificationResponse(success: true); } } class VerificationResponse { final bool success; VerificationResponse({required this.success}); } ``` ### Marketing #### Referral Link Generator & Handler Create and handle referral links with reward tracking ```dart import 'package:redirectly_sdk/redirectly_sdk.dart'; import 'package:share_plus/share_plus.dart'; class ReferralManager { final redirectly = RedirectlySDK(); // Generate referral link Future generateReferralLink(String userId) async { final link = await redirectly.createDeepLink( route: '/referral', data: { 'referrer_id': userId, 'timestamp': DateTime.now().toIso8601String(), }, ); return link; } // Share referral link Future shareReferralLink(String userId) async { final link = await generateReferralLink(userId); await Share.share( 'Join me on the app! $link', subject: 'Join my referral program', ); } // Handle incoming referral Future handleReferral(String? referrerId) async { if (referrerId != null) { // Save referrer info await saveReferrerInfo(referrerId); // Track referral event await redirectly.trackEvent( name: 'referral_accepted', data: {'referrer_id': referrerId}, ); // Award bonus await awardReferralBonus(); } } Future saveReferrerInfo(String referrerId) async { // Save to local storage or API } Future awardReferralBonus() async { // Award points, credits, etc. } } ``` ### Analytics #### Campaign Attribution Reader Read and track attribution data from deep links ```dart import 'package:redirectly_sdk/redirectly_sdk.dart'; class AttributionReader { final redirectly = RedirectlySDK(); Future getAttributionData() async { try { // Get installation attribution final attribution = await redirectly.getAttribution(); return AttributionData( source: attribution['utm_source'] ?? 'organic', medium: attribution['utm_medium'] ?? 'direct', campaign: attribution['utm_campaign'] ?? 'none', content: attribution['utm_content'], term: attribution['utm_term'], clickId: attribution['click_id'], customData: attribution['custom_data'] ?? {}, ); } catch (e) { print('Attribution error: $e'); rethrow; } } Future trackAttributionEvent( String eventName, AttributionData attribution, ) async { await redirectly.trackEvent( name: eventName, data: { 'utm_source': attribution.source, 'utm_medium': attribution.medium, 'utm_campaign': attribution.campaign, 'utm_content': attribution.content, 'utm_term': attribution.term, }, ); } Future analyzeUserJourney(AttributionData attribution) async { print('User came from: ${attribution.source}'); print('Campaign: ${attribution.campaign}'); // Store for analytics await _storeAttributionLocally(attribution); } Future _storeAttributionLocally(AttributionData attribution) async { // Save to local database or preferences } } class AttributionData { final String source; final String medium; final String campaign; final String? content; final String? term; final String? clickId; final Map customData; AttributionData({ required this.source, required this.medium, required this.campaign, this.content, this.term, this.clickId, required this.customData, }); } ``` ### Engagement #### Push Notification Deep Link Handler Handle deep links from push notifications ```dart import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:redirectly_sdk/redirectly_sdk.dart'; class PushNotificationHandler { final messaging = FirebaseMessaging.instance; final redirectly = RedirectlySDK(); Future initializePushNotifications() async { // Request permissions await messaging.requestPermission(); // Handle notification when app is terminated final initialMessage = await messaging.getInitialMessage(); if (initialMessage != null) { _handleNotification(initialMessage); } // Handle notification when app is in foreground FirebaseMessaging.onMessage.listen((RemoteMessage message) { _handleNotification(message); }); // Handle notification tap when app is in background FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { _handleNotification(message); }); } void _handleNotification(RemoteMessage message) { final deepLink = message.data['deep_link']; final screenName = message.data['screen']; if (deepLink != null) { // Navigate via deep link navigateTo(deepLink); } else if (screenName != null) { // Navigate to specific screen navigateToScreen(screenName, message.data); } } void navigateTo(String deepLink) { // Your navigation logic print('Navigating to: $deepLink'); } void navigateToScreen(String screen, Map data) { // Navigate with parameters print('Navigating to screen: $screen'); } } ``` ### Configuration #### go_router Deep Link Configuration Set up go_router with Redirectly deep links ```dart import 'package:go_router/go_router.dart'; import 'package:redirectly_sdk/redirectly_sdk.dart'; final redirectly = RedirectlySDK(); final appRouter = GoRouter( // Handle initial deep link initialLocation: '/', // Route redirect logic redirect: (context, state) async { // Handle Redirectly deep links final deepLink = state.uri.toString(); if (deepLink.contains('redirectly.app')) { // Let Redirectly SDK handle the routing return await redirectly.handleDeepLink(deepLink); } return null; }, routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/product/:id', builder: (context, state) { final productId = state.pathParameters['id']; return ProductDetailScreen(productId: productId ?? ''); }, ), GoRoute( path: '/referral', builder: (context, state) { final referrerId = state.uri.queryParameters['referrer_id']; return ReferralScreen(referrerId: referrerId); }, ), GoRoute( path: '/verify', builder: (context, state) { final token = state.uri.queryParameters['token']; return EmailVerificationScreen(token: token ?? ''); }, ), GoRoute( path: '/promo/:code', builder: (context, state) { final promoCode = state.pathParameters['code']; return PromoScreen(code: promoCode ?? ''); }, ), ], ); // Handle incoming links Future setupDeepLinkHandling() async { redirectly.onDeepLinkReceived = (deepLink, data) { // Custom handling for specific routes if (deepLink.contains('/product/')) { // Analytics tracking redirectly.trackEvent( name: 'product_opened', data: data, ); } }; } ``` #### Basic SDK Initialization Initialize Redirectly SDK in your Flutter app ```dart import 'package:flutter/material.dart'; import 'package:redirectly_sdk/redirectly_sdk.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly SDK await RedirectlySDK.initialize( apiKey: 'your_api_key_here', subdomain: 'your_subdomain', // Optional: Enable debug logging enableDebugLogging: true, ); runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { late RedirectlySDK _redirectly; @override void initState() { super.initState(); _initializeRedirectly(); } Future _initializeRedirectly() async { _redirectly = RedirectlySDK.instance; // Listen for deep links _redirectly.deepLinkStream.listen((deepLink) { _handleDeepLink(deepLink); }); // Get initial deep link (if app was opened via link) final initialLink = await _redirectly.getInitialDeepLink(); if (initialLink != null) { _handleDeepLink(initialLink); } } void _handleDeepLink(String deepLink) { // Route based on deep link final uri = Uri.parse(deepLink); print('Deep link received: ${uri.path}'); // Navigate to appropriate screen } @override Widget build(BuildContext context) { return MaterialApp( title: 'My App', theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomeScreen(), ); } @override void dispose() { _redirectly.dispose(); super.dispose(); } } ``` ## React Native ### Authentication #### Email Verification Deep Link Handler Handle email verification links with token extraction ```typescript import { useEffect } from 'react'; import { Alert } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import dynamicLinks from '@react-native-firebase/dynamic-links'; interface VerificationParams { token: string; } export const useEmailVerificationHandler = () => { const navigation = useNavigation(); useEffect(() => { const unsubscribe = dynamicLinks().onLink(async (link) => { const url = new URL(link.url); if (url.pathname === '/verify') { const token = url.searchParams.get('token'); if (token) { try { const response = await verifyEmail(token); if (response.success) { Alert.alert('Success', 'Email verified successfully'); navigation.navigate('EmailVerified'); } else { Alert.alert('Error', 'Failed to verify email'); } } catch (error) { console.error('Verification error:', error); Alert.alert('Error', 'An error occurred during verification'); } } } }); // Handle initial link dynamicLinks() .getInitialLink() .then((link) => { if (link?.url) { const url = new URL(link.url); if (url.pathname === '/verify') { const token = url.searchParams.get('token'); if (token) handleVerification(token); } } }); return unsubscribe; }, [navigation]); }; async function verifyEmail(token: string) { const response = await fetch('/api/verify-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }); return response.json(); } async function handleVerification(token: string) { // Handle verification logic } ``` ### Marketing #### Referral Link Handler Manage referral links and attribution ```typescript import { useEffect } from 'react'; import { Share, Alert } from 'react-native'; import dynamicLinks from '@react-native-firebase/dynamic-links'; interface ReferralData { referrerId: string; timestamp: string; rewardAmount?: number; } export const useReferralHandler = (userId: string) => { // Generate referral link const generateReferralLink = async (): Promise => { const link = await dynamicLinks().buildShortLink( { link: `https://redirectly.app/referral?referrer_id=${userId}`, domainUriPrefix: 'https://yourapp.page.link', ios: { bundleId: 'com.example.app' }, android: { packageName: 'com.example.app' }, }, dynamicLinks.ShortLinkType.SHORT, ); return link; }; // Share referral link const shareReferralLink = async () => { try { const link = await generateReferralLink(); await Share.share({ message: `Join me on the app! ${link}`, title: 'Referral Program', url: link, // iOS }); } catch (error) { console.error('Share error:', error); } }; // Handle incoming referral const handleIncomingReferral = async ( referralData: ReferralData, ) => { try { // Save referrer information await saveReferrerInfo(referralData.referrerId); // Track event await trackReferralEvent('referral_accepted', { referrer_id: referralData.referrerId, }); // Award bonus await awardReferralBonus(referralData.rewardAmount || 100); Alert.alert( 'Success', `You earned ${referralData.rewardAmount || 100} points!`, ); } catch (error) { console.error('Referral handling error:', error); } }; // Listen for referral links useEffect(() => { const unsubscribe = dynamicLinks().onLink(async (link) => { const url = new URL(link.url); if (url.pathname === '/referral') { const referrerId = url.searchParams.get('referrer_id'); if (referrerId) { await handleIncomingReferral({ referrerId, timestamp: new Date().toISOString(), }); } } }); return unsubscribe; }, []); return { generateReferralLink, shareReferralLink }; }; async function saveReferrerInfo(referrerId: string) { // Save to AsyncStorage or API } async function trackReferralEvent( eventName: string, data: Record, ) { // Track with analytics } async function awardReferralBonus(amount: number) { // Award points/credits } ``` ### Analytics #### Attribution Data Reader Extract and track attribution data from deep links ```typescript import { useEffect, useState } from 'react'; import dynamicLinks from '@react-native-firebase/dynamic-links'; export interface AttributionData { source: string; medium: string; campaign: string; content?: string; term?: string; clickId?: string; customData: Record; } export const useAttributionReader = () => { const [attribution, setAttribution] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const readAttribution = async () => { try { // Get dynamic link data const link = await dynamicLinks().getInitialLink(); if (link) { const url = new URL(link.url); const params = url.searchParams; const attributionData: AttributionData = { source: params.get('utm_source') || 'organic', medium: params.get('utm_medium') || 'direct', campaign: params.get('utm_campaign') || 'none', content: params.get('utm_content') || undefined, term: params.get('utm_term') || undefined, clickId: params.get('click_id') || undefined, customData: { deviceId: link.utmParameters?.deviceId, timestamp: new Date().toISOString(), }, }; setAttribution(attributionData); // Track the attribution event await trackAttributionEvent('app_opened', attributionData); } } catch (error) { console.error('Attribution reading error:', error); } finally { setLoading(false); } }; readAttribution(); // Also listen for links received while app is open const unsubscribe = dynamicLinks().onLink((link) => { const url = new URL(link.url); const params = url.searchParams; const newAttribution: AttributionData = { source: params.get('utm_source') || 'organic', medium: params.get('utm_medium') || 'direct', campaign: params.get('utm_campaign') || 'none', content: params.get('utm_content') || undefined, term: params.get('utm_term') || undefined, clickId: params.get('click_id') || undefined, customData: {}, }; setAttribution(newAttribution); }); return unsubscribe; }, []); return { attribution, loading }; }; async function trackAttributionEvent( eventName: string, attribution: AttributionData, ) { // Send to your analytics provider console.log('Tracking event:', { event: eventName, utm_source: attribution.source, utm_medium: attribution.medium, utm_campaign: attribution.campaign, }); } ``` ### Engagement #### Push Notification Deep Link Handler Handle deep links from push notifications ```typescript import { useEffect } from 'react'; import { Alert } from 'react-native'; import messaging from '@react-native-firebase/messaging'; import { useNavigation } from '@react-navigation/native'; interface NotificationPayload { deepLink?: string; screen?: string; [key: string]: unknown; } export const usePushNotificationHandler = () => { const navigation = useNavigation(); useEffect(() => { // Request permission for notifications messaging().requestPermission(); // Handle notification when app is in foreground const unsubscribeForeground = messaging().onMessage( async (remoteMessage) => { const deepLink = (remoteMessage.data as NotificationPayload) ?.deepLink; const screen = (remoteMessage.data as NotificationPayload)?.screen; if (deepLink) { handleDeepLink(deepLink, navigation); } else if (screen) { navigateToScreen(screen, remoteMessage.data, navigation); } Alert.alert( remoteMessage.notification?.title || 'Notification', remoteMessage.notification?.body, ); }, ); // Handle notification when app is opened from background const unsubscribeBackground = messaging().onNotificationOpenedApp((remoteMessage) => { const deepLink = (remoteMessage.data as NotificationPayload) ?.deepLink; const screen = (remoteMessage.data as NotificationPayload)?.screen; if (deepLink) { handleDeepLink(deepLink, navigation); } else if (screen) { navigateToScreen(screen, remoteMessage.data, navigation); } }); // Check if app was opened from a notification messaging() .getInitialNotification() .then((remoteMessage) => { if (remoteMessage) { const deepLink = (remoteMessage.data as NotificationPayload) ?.deepLink; const screen = (remoteMessage.data as NotificationPayload)?.screen; if (deepLink) { handleDeepLink(deepLink, navigation); } else if (screen) { navigateToScreen(screen, remoteMessage.data, navigation); } } }); return () => { unsubscribeForeground(); unsubscribeBackground(); }; }, [navigation]); }; function handleDeepLink( deepLink: string, navigation: ReturnType, ) { const url = new URL(deepLink); const route = url.pathname; const params = Object.fromEntries(url.searchParams); navigation.navigate(route, params); } function navigateToScreen( screen: string, data: Record, navigation: ReturnType, ) { navigation.navigate(screen, data); } ``` ### Configuration #### React Navigation Deep Link Config Configure React Navigation for Redirectly deep links ```typescript import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import dynamicLinks from '@react-native-firebase/dynamic-links'; import { useEffect } from 'react'; const Stack = createNativeStackNavigator(); const linking = { prefixes: ['https://yourapp.page.link', 'yourapp://'], config: { screens: { Home: '/', ProductDetail: '/product/:id', Referral: { path: '/referral', parse: { referrer_id: String }, }, EmailVerification: { path: '/verify', parse: { token: String }, }, PromoScreen: { path: '/promo/:code', parse: { code: String }, }, }, }, }; export function RootNavigator() { const navigationRef = React.useRef(null); useEffect(() => { const unsubscribe = dynamicLinks().onLink((link) => { // Handle deep link const url = link.url; console.log('Deep link received:', url); }); // Check if app was opened from a deep link dynamicLinks() .getInitialLink() .then((link) => { if (link?.url) { console.log('Initial deep link:', link.url); } }); return unsubscribe; }, []); return ( } ref={navigationRef} > ({ title: `Product ${route.params.id}`, })} /> ); } ``` #### Basic SDK Initialization Initialize Redirectly SDK in your React Native app ```typescript import React, { useEffect } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import dynamicLinks from '@react-native-firebase/dynamic-links'; interface RedirectlyConfig { apiKey: string; subdomain: string; enableDebugLogging?: boolean; } class RedirectlySDK { private static instance: RedirectlySDK; private config: RedirectlyConfig; private constructor(config: RedirectlyConfig) { this.config = config; this.initialize(); } static getInstance(config: RedirectlyConfig): RedirectlySDK { if (!RedirectlySDK.instance) { RedirectlySDK.instance = new RedirectlySDK(config); } return RedirectlySDK.instance; } private initialize() { if (this.config.enableDebugLogging) { console.log('Redirectly SDK initialized'); console.log('API Key:', this.config.apiKey.substring(0, 10) + '...'); console.log('Subdomain:', this.config.subdomain); } } async handleDeepLink(url: string): Promise { try { const deepLink = new URL(url); if (this.config.enableDebugLogging) { console.log('Processing deep link:', deepLink.pathname); } return true; } catch (error) { console.error('Deep link handling error:', error); return false; } } async trackEvent( eventName: string, data?: Record, ): Promise { if (this.config.enableDebugLogging) { console.log('Tracking event:', eventName, data); } // Send to analytics } } // Initialize in your app const Stack = createNativeStackNavigator(); const redirectly = RedirectlySDK.getInstance({ apiKey: 'your_api_key_here', subdomain: 'your_subdomain', enableDebugLogging: true, }); export function App() { useEffect(() => { // Handle initial deep link dynamicLinks() .getInitialLink() .then(async (link) => { if (link?.url) { await redirectly.handleDeepLink(link.url); } }); // Listen for deep links while app is open const unsubscribe = dynamicLinks().onLink(async (link) => { await redirectly.handleDeepLink(link.url); }); return unsubscribe; }, []); return ( ); } // Dummy screens function HomeScreen() { return null; } function DetailsScreen() { return null; } ``` ## Code Snippets for Every Use Case Our comprehensive library of code snippets covers the most common deep linking scenarios. Whether you're implementing email verification, tracking referrals, handling push notifications, or reading attribution data, we have production-ready code that you can copy and paste into your project. Each snippet is fully commented and includes error handling. We provide examples for both Flutter (Dart) and React Native (TypeScript) so you can choose the framework that works best for your team. The snippets demonstrate best practices and integration with popular libraries like go_router, React Navigation, Firebase, and OneSignal. All code is maintained and updated to work with the latest SDK versions. You can customize the snippets to match your specific needs, such as changing API endpoints, modifying navigation flows, or integrating with your own analytics platform. We recommend reviewing the code and testing thoroughly in your development environment before deploying to production. ## Related - [Flutter Guide](https://redirectly.app/flutter-deferred-deep-linking.md) — Complete guide to implementing deep linking in Flutter apps with Redirectly - [React Native Guide](https://redirectly.app/react-native-deferred-deep-linking.md) — Step-by-step instructions for React Native deep linking with Redirectly - [Deep Linking 101](https://redirectly.app/deep-linking-guide.md) — Learn the fundamentals of deep linking and best practices ## Need Custom Implementation? While our snippets cover common scenarios, every app is unique. Our team can help you customize the implementation for your specific use case, integrate with your backend, or set up advanced attribution tracking. - [Contact Us](https://redirectly.app/contact) - [View Integrations](https://redirectly.app/integrations.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Linking Guide > A practical deep linking guide covering Universal Links, App Links, and deferred deep linking, plus how to implement them with Redirectly for Flutter and RN. - Canonical: https://redirectly.app/deep-linking-guide - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Deep linking sends users from a web link (or another app) into a specific screen inside your app. This guide covers the basics and how Redirectly fits in for Flutter and React Native. ## In this article - What is deep linking? - Universal Links and App Links - Deferred deep linking - Using Redirectly ## What is deep linking? Deep linking uses a URL to open your app and, ideally, a specific route (e.g. a product page or campaign). On iOS you use Universal Links (https); on Android you use App Links. Both require hosting a file on your domain (apple-app-site-association, assetlinks.json) so the OS can verify that your app is allowed to handle that domain. For a full primer, see [What is Deep Linking?](https://redirectly.app/blog/what-is-deep-linking.md) on our blog. - **Universal Links (iOS)** — HTTPS links that open your app when tapped. Require apple-app-site-association on your domain. - **App Links (Android)** — HTTPS links that open your app. Require assetlinks.json on your domain and intent filters in your app. ## Universal Links and App Links Both iOS and Android require you to prove that your app owns the domain used in the link. You do that by hosting a well-known file on that domain and declaring your app's bundle ID or package name. The OS then routes links for that domain to your app instead of the browser. - iOS: Add Associated Domains (e.g. applinks:yourapp.redirectly.app) and host apple-app-site-association at /.well-known/apple-app-site-association. - Android: Add an intent filter for your domain and host assetlinks.json at /.well-known/assetlinks.json. - Redirectly hosts these files for your subdomain (e.g. yourapp.redirectly.app) so you don't have to manage them yourself. ## Deferred deep linking If the user doesn't have your app yet, a normal deep link can't open it. Deferred deep linking solves that: the link sends them to the store, and after install the app receives the original link data so you can route them to the right content. That's what Redirectly is built for. [Deferred deep linking](https://redirectly.app/deferred-deep-linking.md) and [What is Deferred Deep Linking?](https://redirectly.app/blog/what-is-deferred-deep-linking.md) go deeper. ## Using Redirectly Redirectly gives you a subdomain (e.g. yourapp.redirectly.app), hosts the required association files, and provides Flutter and React Native SDKs. You configure Universal Links and App Links once, then create links via API or dashboard. No native code needed for the SDK integration. - [Flutter deferred deep linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Pure Dart SDK, no native code required. - [React Native deferred deep linking](https://redirectly.app/react-native-deferred-deep-linking.md) — TypeScript SDK, Expo and bare RN. ### Related resources - [What is deep linking?](https://redirectly.app/blog/what-is-deep-linking.md) on our blog - [Apple App Site Association (AASA) validator](https://redirectly.app/aasa-validator.md) — Validate your Universal Links setup - [assetlinks.json validator](https://redirectly.app/assetlinks-validator.md) — Validate your App Links setup --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deferred Deep Linking > Deferred deep linking lets users land on the right content after installing your app. Learn how it works and how Redirectly delivers it for Flutter and RN. - Canonical: https://redirectly.app/deferred-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Deferred deep linking lets your link survive app installation. Users who don't have your app yet get sent to the store; after they install and open the app, they land on the content the link pointed to. Here's how it works and how Redirectly delivers it. ## In this article - What is deferred deep linking? - Deferred vs regular deep linking - How it works (step by step) - Use cases and benefits - How Redirectly does it ## What is deferred deep linking? Deferred deep linking is a technology that lets a user click a link, install your app, and then be taken to the specific content they originally intended—even if they didn't have the app when they first clicked. The deep link action is "deferred" until after installation is complete. - **Regular deep linking** — Only works if the app is already installed. User clicks link → App opens (if installed). Fails if app not installed. - **Deferred deep linking** — Works whether the app is installed or not, with attribution. User clicks link → Install app → Navigate to content. Always works with attribution. ## Deferred vs regular deep linking Regular deep linking opens your app and routes the user to a screen when they already have the app installed. Deferred deep linking adds two things: (1) redirecting users without the app to the store, and (2) after install, matching the user back and routing them to the intended content. That second part requires storing the link intent and matching the device after install—which is what services like Redirectly provide. - Regular: link works only when app is installed. - Deferred: link works always; if not installed, user goes to store, then after install they land on the right content. - Deferred also gives you install attribution: you know which link or campaign drove the install. ## How deferred deep linking works (step by step) The flow is the same across providers: capture the click, store the intent, redirect to the store, then match and route after install. 1. **User clicks link** — User taps a link from email, social, ad, or QR code. The link points to your subdomain (e.g. yourapp.redirectly.app). 2. **Attribution tracking** — The service creates a device fingerprint and stores the intended destination (e.g. product ID, campaign ID) linked to that fingerprint. 3. **App Store redirect** — If the app isn't installed, the user is redirected to the App Store or Play Store. The link intent is stored server-side. 4. **User installs and opens app** — User installs the app and opens it for the first time. 5. **Attribution match** — The app SDK checks with the service. The service matches the device to the stored click and returns the original link data. 6. **Deferred navigation** — Your app routes the user to the specific content they originally intended—product page, campaign landing, etc. ## Use cases and benefits Deferred deep linking powers better conversion, attribution, and retention across marketing and product flows. - **Campaign attribution** — Know which ads, emails, or referrals drove each install. Optimize spend and creative based on real data. - **Seamless onboarding** — Send users from a link to a specific screen after install—product page, promo, or signup flow—so first-run experience matches intent. - **Referral and sharing** — Referral links and social shares that survive install so you can credit the referrer and route the new user correctly. ## How Redirectly does it Redirectly provides deferred deep linking for Flutter and React Native with pure Dart and TypeScript SDKs—no native code required for basic setup. You get a subdomain (e.g. yourapp.redirectly.app), configure Universal Links and App Links once, and create links via API or dashboard. After a user clicks, we handle fingerprinting, store redirect, and match on first open so your app receives the original link data and can route the user. For a full explainer with examples, read [What is Deferred Deep Linking?](https://redirectly.app/blog/what-is-deferred-deep-linking.md) on our blog. See also our [deep linking guide](https://redirectly.app/deep-linking-guide.md) and [Firebase Dynamic Links alternative](https://redirectly.app/firebase-dynamic-links-alternative.md). - [Flutter deferred deep linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native deferred deep linking](https://redirectly.app/react-native-deferred-deep-linking.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Flutter Deep Linking Tutorial > Master Flutter deep linking and deferred deep linking. Learn iOS Universal Links, Android App Links, install attribution, go_router, and Firebase migration. - Canonical: https://redirectly.app/flutter-deferred-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Master deferred deep linking, iOS Universal Links, Android App Links, and install attribution. Complete step-by-step guide with code examples for flutter_redirectly SDK. ## In this article - Introduction: Why Deep Linking Matters - Prerequisites & Requirements - Step 1: iOS Universal Links Setup - Step 2: Android App Links Setup - Step 3: SDK Installation & Initialization - Step 4: go_router Integration - Step 5: Handling Deferred Deep Links - Step 6: Testing & Debugging - Migration from Firebase Dynamic Links - Frequently Asked Questions ## Why Deep Linking Matters for Your Flutter App Deep linking is the cornerstone of modern mobile app growth. When a user clicks a link to specific content in your app, they should land directly on that screen—whether the app is already installed or not. This seamless experience is critical for user retention, engagement, and understanding which marketing campaigns drive installs. Traditional deep links work fine if the app is installed: the OS routes the link directly to your app, and you parse the URL to navigate to the right screen. But what happens when a user hasn't installed your app yet? They click a marketing link, get redirected to the App Store or Google Play, install the app, and open it—only to land on the home screen, losing all context about what they originally intended to view. This is where **deferred deep linking** changes the game. ### How Deferred Deep Linking Works 1. **User clicks link** — User taps a Redirectly link (e.g., yourapp.redirectly.app/product/123) in a browser, email, or social media. 2. **Redirectly captures intent** — Redirectly servers capture the click, the URL parameters, UTM data, device info, and geo location. This becomes the "deferred link data." 3. **App not installed?** — If the app isn't installed, the user is redirected to the App Store or Google Play (or a custom landing page). 4. **User installs & opens app** — The user installs and launches your app for the first time. 5. **SDK retrieves deferred data** — Your flutter_redirectly SDK makes a single API call to Redirectly to fetch the stored link data for this new install. 6. **Route user correctly** — Your app routes the user to the exact screen they intended (e.g., product/123) with full context restored. - **Deferred Deep Linking** — Links work before AND after app install. Users always land on the right screen, even if the app wasn't installed when they clicked. - **Install Attribution** — Understand which marketing campaigns, channels, and links drive app installs. Track conversions from first click to purchase. - **Pure Dart SDK** — No native Kotlin or Swift code required. One line of initialization in main.dart and you're ready. - **iOS Universal Links** — Secure, native deep linking on iOS via apple-app-site-association. No Safari redirects or user prompts. - **Android App Links** — Verified deep linking on Android via assetlinks.json. Direct app opening without browser fallback. ## Prerequisites & Requirements Before you begin implementing deferred deep linking in your Flutter app, ensure you have the following in place: - **Flutter 3.0+** — flutter_redirectly requires Flutter 3.0 or later. Run `flutter --version` to check. - **Dart 3.0+** — The SDK uses modern Dart null-safety features. Your project must target Dart 3.0 or later. - **Redirectly Account** — Sign up for free at [redirectly.app](https://redirectly.app) and create an API key. - **Custom Subdomain** — Configure your custom subdomain (e.g., yourapp.redirectly.app) in your Redirectly dashboard. This will be used for all deep links. - **iOS Development Environment** — Xcode 14+, an Apple Developer account, and a provisioning profile for your app. - **Android Development Environment** — Android Studio, SDK Platform 21+, and a registered app in your Google Play Console. ## iOS Universal Links Setup iOS uses **Universal Links** to route web URLs to your app. Universal Links are cryptographically verified associations between your domain and your app, defined by a file called `apple-app-site-association` (AASA) on your server. Redirectly hosts this file for you, so you don't need to set up your own server. ### Step 1.1: Enable Associated Domains in Xcode First, enable the Associated Domains capability in your iOS project: 1. Open **ios/Runner.xcworkspace** in Xcode (not Runner.xcodeproj). 2. Select the **Runner** target. 3. Go to **Signing & Capabilities** tab. 4. Click **+ Capability** and search for "Associated Domains". 5. Click to add the capability to your target. ### Step 1.2: Add Your Domain to Runner.entitlements After enabling the capability, Xcode creates an entitlements file. You need to add your Redirectly subdomain: **ios/Runner/Runner.entitlements** ```xml com.apple.developer.associated-domains applinks:yourapp.redirectly.app webcredentials:yourapp.redirectly.app ``` Replace `yourapp.redirectly.app` with your actual Redirectly subdomain configured in the dashboard. ### Step 1.3: Verify AASA File Configuration Redirectly automatically hosts the `apple-app-site-association` file for your subdomain. To verify it's correctly configured: 1. Go to the [AASA Validator Tool](https://redirectly.app/aasa-validator.md). 2. Enter your subdomain (e.g., yourapp.redirectly.app). 3. The tool will fetch and validate the AASA file. You should see your app ID listed in the apps section. **HTTPS Required** — Universal Links only work with HTTPS domains. Redirectly uses HTTPS for all subdomains, so this is handled automatically. ### Step 1.4: What the AASA File Contains Here's an example of what Redirectly serves for your AASA configuration (you don't need to create this—Redirectly does it automatically): **Example AASA File** ```json { "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.yourcompany.yourapp", "paths": ["/*"] } ] } } ``` The AASA file tells iOS: "Any URL matching yourapp.redirectly.app/* should be opened with the app ID TEAMID.com.yourcompany.yourapp, not Safari." ## Android App Links Setup Android uses **App Links** for verified deep linking. App Links require an intent filter in your AndroidManifest.xml and a cryptographically verified JSON file (`assetlinks.json`) served from your domain. Like AASA, Redirectly hosts assetlinks.json for you. ### Step 2.1: Add Intent Filter to AndroidManifest.xml Add an intent filter to the activity that handles deep links (usually MainActivity). The `autoVerify="true"` attribute tells Android to verify the domain: **android/app/src/main/AndroidManifest.xml** ```xml ``` Replace `yourapp.redirectly.app` with your actual Redirectly subdomain. The `android:exported="true"` is required for activities that can be launched by other apps. ### Step 2.2: Verify assetlinks.json Configuration Redirectly automatically hosts the `assetlinks.json` file. Verify it's correctly configured: 1. Go to the [assetlinks.json Validator Tool](https://redirectly.app/assetlinks-validator.md). 2. Enter your subdomain (e.g., yourapp.redirectly.app). 3. The tool will fetch and validate the file. You should see your package name and SHA256 fingerprint listed. ### Step 2.3: What the assetlinks.json File Contains Here's an example of what Redirectly serves (you don't need to create this): **Example assetlinks.json** ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.yourcompany.yourapp", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ] } } ] ``` The assetlinks.json file tells Android: "The app with package name com.yourcompany.yourapp and this SHA256 certificate is allowed to handle URLs for yourapp.redirectly.app." **Certificate Fingerprint Must Match** — The SHA256 certificate fingerprint in assetlinks.json must match the certificate you use to sign your APK/AAB. If you release a new build signed with a different key, you'll need to update the fingerprint. ## SDK Installation & Initialization ### Step 3.1: Add flutter_redirectly to pubspec.yaml **pubspec.yaml** ```yaml dependencies: flutter: sdk: flutter flutter_redirectly: ^2.1.6 ``` Then run `flutter pub get` to install the package. ### Step 3.2: Initialize Redirectly in main.dart Initialize Redirectly as early as possible in your app's lifecycle, ideally in main() or before your app runs: **lib/main.dart** ```dart import 'package:flutter/material.dart'; import 'package:flutter_redirectly/flutter_redirectly.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly final redirectly = Redirectly.instance; await redirectly.initialize( RedirectlyConfig( apiKey: 'your-api-key', subdomain: 'yourapp.redirectly.app', debug: true, // Set to false in production ), ); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Your App', home: const HomePage(), ); } } ``` Replace `your-api-key` with your API key from the Redirectly dashboard, and `yourapp.redirectly.app` with your actual subdomain. ### Step 3.3: Configure Redirect Handling After initialization, configure how your app handles incoming deep links: **lib/main.dart (continued)** ```dart // Listen for incoming deep links redirectly.linkStream.listen((link) { print('Received deep link: ${link.url}'); print('Parameters: ${link.parameters}'); // Handle the link and navigate in your app }); // Listen for deferred deep links (new install) redirectly.onAppInstalled.listen((link) { print('App just installed from link: ${link.url}'); // Navigate to the deferred content }); ``` ## go_router Integration `go_router` is the modern routing solution for Flutter apps. It provides declarative routing with deep link support. Integrating Redirectly with go_router is straightforward. ### Step 4.1: Configure go_router with Deep Link Routes First, add go_router to your pubspec.yaml: ```yaml dependencies: go_router: ^13.0.0 ``` ### Step 4.2: Set Up go_router with Redirectly **lib/router.dart** ```dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_redirectly/flutter_redirectly.dart'; final goRouter = GoRouter( routes: [ GoRoute( path: '/', name: 'home', builder: (context, state) => const HomePage(), routes: [ GoRoute( path: 'product/:id', name: 'product', builder: (context, state) { final productId = state.pathParameters['id']; return ProductDetailPage(productId: productId ?? ''); }, ), GoRoute( path: 'user/:userId', name: 'user', builder: (context, state) { final userId = state.pathParameters['userId']; return UserProfilePage(userId: userId ?? ''); }, ), ], ), ], redirect: _handleRedirect, ); // Handle incoming deep links FutureOr _handleRedirect( BuildContext context, GoRouterState state, ) async { final redirectly = Redirectly.instance; // Check for deferred deep link final deferredLink = await redirectly.getDeferredLink(); if (deferredLink != null) { return deferredLink.deepLinkPath; } return null; } ``` ### Step 4.3: Use go_router in Your App **lib/main.dart** ```dart import 'package:flutter/material.dart'; import 'package:flutter_redirectly/flutter_redirectly.dart'; import 'router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly final redirectly = Redirectly.instance; await redirectly.initialize( RedirectlyConfig( apiKey: 'your-api-key', subdomain: 'yourapp.redirectly.app', ), ); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Your App', routerConfig: goRouter, ); } } ``` ### Step 4.4: Handle Link Events with StreamBuilder For real-time handling of deep links when the app is already running, use a StreamBuilder: ```dart class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final redirectly = Redirectly.instance; return Scaffold( appBar: AppBar(title: const Text('Home')), body: StreamBuilder( stream: redirectly.linkStream, builder: (context, snapshot) { if (snapshot.hasData) { final link = snapshot.data!; // Handle the deep link WidgetsBinding.instance.addPostFrameCallback((_) { context.go(link.deepLinkPath); }); } return const Center( child: Text('Welcome to your app!'), ); }, ), ); } } ``` ## Handling Deferred Deep Links Deferred deep links are the crown jewel of Redirectly. When a user clicks a link before installing your app, the SDK retrieves the deferred data after the first app launch and routes them to the intended screen. ### How Deferred Deep Links Work (Technical Deep Dive) 1. **Link Click** — User taps a Redirectly link (e.g., yourapp.redirectly.app/product/123?utm_source=instagram) in Safari, an email, or social media. 2. **Server-Side Capture** — Redirectly's servers capture: the URL, all query parameters, the user's IP address, User-Agent, geo location, and device info. This is stored as a "pending link". 3. **Redirect to Store** — If the app isn't installed, the user is redirected to the App Store (iOS) or Google Play (Android), or a custom landing page. 4. **App Install & Launch** — The user installs the app from the store and launches it for the first time. 5. **SDK Fetches Deferred Data** — Your flutter_redirectly SDK automatically makes an API call to Redirectly with the device's identifiers (advertising ID, etc.) to retrieve the stored link data. 6. **Match & Route** — Redirectly matches the device and returns the deferred link. Your app parses the URL and routes the user to the exact screen they intended. ### Step 5.1: Retrieve Deferred Link Data The SDK automatically handles most of this, but you need to listen for the deferred link and act on it: **lib/main.dart** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly final redirectly = Redirectly.instance; await redirectly.initialize( RedirectlyConfig( apiKey: 'your-api-key', subdomain: 'yourapp.redirectly.app', ), ); // Listen for deferred deep link redirectly.onAppInstalled.listen((link) { print('Deferred link data:'); print(' URL: ${link.url}'); print(' Path: ${link.deepLinkPath}'); print(' Parameters: ${link.parameters}'); print(' UTM Source: ${link.parameters['utm_source']}'); // Navigate to the deferred content // This will be handled by go_router or your navigation logic }); runApp(const MyApp()); } ``` ### Step 5.2: Access Attribution Data The `RedirectlyLink` object contains all the data you need for attribution: ```dart final link = await redirectly.getDeferredLink(); if (link != null) { // Original URL that was clicked final originalUrl = link.url; // e.g., yourapp.redirectly.app/product/123?utm_source=instagram // Extracted path for routing final deepLinkPath = link.deepLinkPath; // e.g., /product/123 // All query parameters and UTM data final parameters = link.parameters; final utmSource = parameters['utm_source']; // instagram final utmCampaign = parameters['utm_campaign']; // Device and geo info final deviceId = link.deviceId; final geoCountry = link.geoCountry; final geoCity = link.geoCity; // Log this to your analytics analytics.logDeferredLinkReceived( url: originalUrl, utmSource: utmSource, utmCampaign: utmCampaign, ); } ``` ### Step 5.3: Integration with go_router Update your go_router redirect logic to handle deferred links: ```dart FutureOr _handleRedirect( BuildContext context, GoRouterState state, ) async { final redirectly = Redirectly.instance; // Check for deferred deep link on first app launch final deferredLink = await redirectly.getDeferredLink(); if (deferredLink != null) { print('Routing to deferred link: ${deferredLink.deepLinkPath}'); // Optional: Store attribution data _storeAttributionData(deferredLink); // Return the path to navigate to return deferredLink.deepLinkPath; } // Otherwise, check for immediate deep link final immediateLink = await redirectly.getLink(); if (immediateLink != null) { return immediateLink.deepLinkPath; } return null; } void _storeAttributionData(RedirectlyLink link) { // Store the attribution data for later analysis final prefs = SharedPreferences.getInstance(); prefs.then((p) { p.setString('install_source', link.parameters['utm_source'] ?? 'direct'); p.setString('install_campaign', link.parameters['utm_campaign'] ?? ''); }); } ``` **Pro Tip: Attribution Tracking** — Always store the utm_source, utm_campaign, and other attribution data from deferred links. This is crucial for understanding which marketing channels drive installations and conversions. ## Testing & Debugging Testing deep links is crucial before launching your app. Here's how to test on both platforms. ### iOS Simulator Testing iOS simulators have limitations with Universal Links. Use the xcrun simctl command to simulate link clicks: ```bash # Get your simulator UDID xcrun simctl list devices # Open a deep link in the simulator xcrun simctl openurl booted https://yourapp.redirectly.app/product/123 # Simulate app install + open with deferred data xcrun simctl openurl booted https://yourapp.redirectly.app/product/456?utm_source=instagram ``` The `booted` argument targets the currently running simulator. For a specific simulator, use its UDID. ### Android Emulator Testing Android emulators fully support App Links testing. Use adb shell: ```bash # List connected emulators/devices adb devices # Start an app with a deep link intent adb shell am start -a android.intent.action.VIEW \ -d "https://yourapp.redirectly.app/product/123" \ com.yourcompany.yourapp # Test with query parameters adb shell am start -a android.intent.action.VIEW \ -d "https://yourapp.redirectly.app/product/456?utm_source=google" \ com.yourcompany.yourapp ``` ### Real Device Testing For comprehensive testing, use real devices: - **iOS Real Device:** Build and run on a real iPhone. Open links in Safari or any app (Mail, Messages, Twitter, etc.). Universal Links will open your app. - **Android Real Device:** Build and run on a real Android phone. App Links will automatically open your app when you click the link. ### Verify with Redirectly Dashboard The Redirectly dashboard shows real-time analytics for every link you create: 1. Log in to your [Redirectly dashboard](https://dashboard.redirectly.app). 2. Create a test link (e.g., yourapp.redirectly.app/product/123). 3. Click the link on your device and observe the analytics update in real-time. 4. Check logs to see if your app received the link data correctly. ### Common Issues & Troubleshooting #### Link opens in browser instead of app **Cause:** AASA (iOS) or assetlinks.json (Android) is not properly configured or served. **Fix:** Use the [AASA Validator](https://redirectly.app/aasa-validator.md) and [assetlinks Validator](https://redirectly.app/assetlinks-validator.md) to verify your configuration. #### Deferred link data not received **Cause:** SDK not initialized before calling getDeferredLink(), or API key is invalid. **Fix:** Ensure Redirectly.initialize() is called in main() before your app runs. Check that your API key is correct. #### App crashes on deep link **Cause:** Path parameters are null or route doesn't match the deep link path. **Fix:** Add null-safety checks for path parameters in your route builders. Log the incoming link path and verify it matches your go_router routes. #### iOS Universal Links not working **Cause:** Associated Domains capability not enabled, or applinks: prefix is missing. **Fix:** Re-check Step 1.1 and 1.2. Make sure you added the capability in Xcode and the applinks: prefix is in your entitlements file. #### Android App Links not working **Cause:** Intent filter missing android:autoVerify="true", or certificate fingerprint doesn't match. **Fix:** Verify your AndroidManifest.xml has autoVerify=true. Get your APK's SHA256 fingerprint using keytool or Android Studio and ensure it's in assetlinks.json. **Testing Checklist** - AASA file validates successfully - assetlinks.json file validates successfully - Deep link opens app (not browser) on iOS device - Deep link opens app (not browser) on Android device - App routes to correct screen after link click - Deferred link works after fresh install - UTM parameters and query data are captured - Redirectly dashboard shows link clicks and conversions ## Migration from Firebase Dynamic Links Firebase Dynamic Links (FDL) was deprecated in March 2024. If your app uses FDL, migrating to Redirectly is straightforward. The flow is similar, but with a pure Dart SDK and better performance. ### Quick Comparison | Feature | Firebase Dynamic Links | Redirectly | | --- | --- | --- | | SDK Type | Firebase (platform-specific) | Pure Dart | | Native Code Required | Yes (Kotlin/Swift) | No (config only) | | Deferred Deep Linking | Yes | Yes | | Custom Domain | firebaseapp.com only | Custom domain support | | Install Attribution | Basic (Firebase Analytics) | Real-time dashboard | | Maintenance | Deprecated (no updates) | Active support | ### Migration Steps 1. **Remove Firebase** — Remove the firebase_dynamic_links package from pubspec.yaml and all Firebase initialization code. 2. **Add Redirectly** — Follow Step 3 of this guide to add flutter_redirectly to your project. 3. **Update Configuration** — Replace Firebase initialization with Redirectly initialization (see Step 3.2). **Firebase:** Used google-services.json and Firebase Console. **Redirectly:** Simple API key + subdomain configuration. 4. **Update Link Handling** — Replace Firebase Dynamic Links listeners with Redirectly listeners. Old Firebase code: ```dart FirebaseDynamicLinks.instance.onLink.listen((dynamicLink) { // Handle the link }); ``` New Redirectly code: ```dart final redirectly = Redirectly.instance; redirectly.linkStream.listen((link) { // Handle the link }); ``` 5. **Update Platform Config** — Follow Steps 1 and 2 of this guide to set up iOS Universal Links and Android App Links for your Redirectly subdomain. 6. **Update Link Generation** — Update anywhere you generate Firebase Dynamic Links to use Redirectly links instead. **Firebase:** Used DynamicLinkParameters in code. **Redirectly:** Simply format your deep link URLs (e.g., yourapp.redirectly.app/product/123?utm_source=email). 7. **Test & Deploy** — Follow Step 6 of this guide to test deep links on iOS and Android before releasing your app update. For a detailed code comparison and step-by-step walkthrough, see our [Firebase Dynamic Links migration guide](https://redirectly.app/blog/migrating-from-firebase-dynamic-links.md). Also check out our [Firebase Dynamic Links alternative](https://redirectly.app/firebase-dynamic-links-alternative.md) page for more information on why Redirectly is a better choice. ## Frequently Asked Questions ### Does Redirectly require native code for Flutter? No, flutter_redirectly is a pure Dart SDK. You don't need to write any Kotlin or Swift code. The only native configuration required is: - iOS: Add applinks entry to Runner.entitlements (XML config) - Android: Add intent filter to AndroidManifest.xml (XML config) These are simple configuration files, not code. ### Does Redirectly work with go_router? Yes, Redirectly integrates seamlessly with go_router. See Step 4 for a complete example showing how to configure go_router with Redirectly deep link handling, including deferred link support in the redirect callback. ### How does deferred deep linking work? When a user clicks a Redirectly link before installing the app, Redirectly's servers capture the URL and all parameters. After the user installs and opens the app, the flutter_redirectly SDK retrieves this captured data and passes it to your app, allowing you to route the user to the intended screen. See the "How Deferred Deep Linking Works" section for technical details. ### Can I test deep links on iOS simulator? iOS simulators have limitations with Universal Links, but you can simulate link clicks using the xcrun simctl command: ```bash xcrun simctl openurl booted https://yourapp.redirectly.app/product/123 ``` For comprehensive testing, use a real iOS device. See Step 6 for more details. ### How is Redirectly different from Firebase Dynamic Links? Firebase Dynamic Links was deprecated in March 2024. Redirectly is a modern alternative with a pure Dart SDK (no native code required), better performance, real-time analytics dashboard, active support, and custom domain support. See the migration guide for a detailed comparison. ### Does Redirectly support custom domains? Yes, you can use your own domain (e.g., links.yourcompany.com) instead of the default redirectly.app domain. Configure it in your Redirectly account settings and add the appropriate DNS records. The setup is the same—your domain will host the AASA and assetlinks.json files automatically. ### What happens if AASA or assetlinks.json is misconfigured? If these files are missing or incorrect, the OS will not recognize your domain as a verified app domain. When users click your links, they'll open in the browser instead of your app. Use our validation tools ([AASA Validator](https://redirectly.app/aasa-validator.md) and [assetlinks Validator](https://redirectly.app/assetlinks-validator.md)) to verify your configuration. ### Can I use Redirectly with other deep linking solutions? Yes, Redirectly can work alongside other solutions. However, if you're using Firebase Dynamic Links, we recommend fully migrating to Redirectly (the FDL API is deprecated anyway). For custom deep link schemes, Redirectly's Universal Links and App Links approach is more reliable and doesn't conflict with other solutions. ## Next Steps You're now equipped with everything you need to implement deferred deep linking in your Flutter app. Here's what to do next: 1. **Create Your Account** — Sign up for free at redirectly.app and configure your custom subdomain. [Get started](https://redirectly.app/signup) 2. **Follow This Guide** — Implement the 6 steps in order: iOS, Android, SDK, go_router, deferred links, testing. Start with Step 1. 3. **Read the Docs** — Deep dive into the Redirectly SDK documentation for advanced features. [View Flutter docs](https://docs.redirectly.app/flutter) **Questions or issues?** Check out our [general deep linking guide](https://redirectly.app/deep-linking-guide.md), [React Native deep linking guide](https://redirectly.app/react-native-deferred-deep-linking.md), or reach out to our support team. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # React Native Deep Linking Tutorial > Complete React Native deep linking tutorial with the Redirectly SDK. Learn deferred deep linking, install attribution, and React Navigation v6. Expo ready. - Canonical: https://redirectly.app/react-native-deferred-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete guide to deferred deep linking in React Native with Redirectly: pure TypeScript SDK, works with Expo and bare React Native. Includes iOS Universal Links, Android App Links, install attribution, React Navigation v6 integration, and testing. ## In this article - Introduction - Prerequisites - iOS Universal Links Setup - Android App Links Setup - SDK Installation & Initialization - React Navigation v6 Integration - Deferred Deep Link Handling - Testing & Debugging - Expo Configuration - Migration from Firebase Dynamic Links - FAQ ## Why Deep Linking Matters for React Native Apps Deep linking allows users to jump directly to specific content in your app from external sources—emails, SMS, web pages, ads, and social media. Instead of landing on the app home screen, users land exactly where you want them, dramatically improving user experience and conversion rates. **Deferred deep linking** takes this further: links work even if the app isn't installed yet. When a user clicks a link, we detect the install, and after the app opens for the first time, the SDK retrieves the original link data and routes the user to the correct destination. This is crucial for acquisition campaigns, app launch promotions, and referral programs. The React Native Redirectly SDK (`react-native-redirectly`) is a pure TypeScript implementation with full type definitions. It works seamlessly with both bare React Native and Expo, and handles the entire deep linking flow—no native code changes required for the SDK itself. - **Deferred deep linking** — Links survive app install and route users to the right content. - **Install attribution** — Match installs to link clicks and campaigns. - **TypeScript** — Full type definitions, works with Expo and bare RN. ## Before you start This guide assumes you have a React Native project set up and running. Here's what you need: - React Native 0.61+ (0.70+ recommended) - React Navigation v5+ (v6 recommended for best integration) - Redirectly account and API key (sign up free at redirectly.app) - Redirectly subdomain (e.g., myapp.redirectly.app) - For bare RN: Xcode (iOS) and Android Studio (Android) - For Expo: EAS Build (optional, for production deep linking) - AsyncStorage (optional but recommended for install attribution persistence) This guide covers both Expo and bare React Native. Some sections apply only to bare RN (iOS Universal Links, Android App Links). Expo users can skip directly to the Expo Configuration section. ## iOS Universal Links Setup iOS uses Universal Links to route deep links to your app. Universal Links require configuration in Xcode, an entitlements file, and an AASA (apple-app-site-association) file hosted on your domain. Redirectly automatically hosts the AASA file for your subdomain. ### Step 1a: Xcode Associated Domains Configuration Open your project in Xcode, select the target, and go to Signing & Capabilities. Add the Associated Domains capability (+ Capability button). **Associated Domains Capability** ```text applinks:myapp.redirectly.app webcredentials:myapp.redirectly.app ``` Replace myapp with your Redirectly subdomain. ### Step 1b: Entitlements File The associated domains must also be declared in your entitlements file. Xcode usually manages this, but you can verify or manually add to your {ProjectName}.entitlements file: **{ProjectName}.entitlements** ```xml com.apple.developer.associated-domains applinks:myapp.redirectly.app webcredentials:myapp.redirectly.app ``` Replace myapp with your Redirectly subdomain. ### Step 1c: AppDelegate Configuration (Optional) If you're using React Navigation, most handling is done in the Navigation linking config (see React Navigation section below). However, you may need to handle the app delegate method for certain scenarios: **AppDelegate.swift (Optional)** ```swift import UIKit import React @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { // React Navigation and Redirectly SDK handle this via linking config return true } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { // Handle universal links - React Navigation handles this return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) } // ... rest of AppDelegate } ``` ### Step 1d: AASA Verification Redirectly automatically hosts the apple-app-site-association file at `https://myapp.redirectly.app/.well-known/apple-app-site-association`. Verify it's correct and properly signed using our [AASA validator tool](https://redirectly.app/aasa-validator.md). ## Android App Links Setup Android uses App Links to route deep links to your app. Unlike Universal Links, App Links require digital asset verification via an assetlinks.json file. Redirectly hosts this file for your subdomain, and you declare the link intent filter in AndroidManifest.xml. ### Step 2a: Intent Filter in AndroidManifest.xml Add an intent filter to your MainActivity in `android/app/src/main/AndroidManifest.xml`: **AndroidManifest.xml** ```xml ``` Replace myapp.redirectly.app with your Redirectly subdomain. The `android:autoVerify="true"` attribute triggers automatic verification of the assetlinks.json file. ### Step 2b: assetlinks.json Verification Redirectly automatically hosts the assetlinks.json file at `https://myapp.redirectly.app/.well-known/assetlinks.json`. Verify it's correct using our [assetlinks validator tool](https://redirectly.app/assetlinks-validator.md). You'll need your app's signing certificate SHA256 fingerprint, which you can find in your Android keystore. **Get your SHA256 fingerprint** ```bash keytool -list -v -keystore android/app/debug.keystore -alias androiddebugkey -storepass android -keypass android ``` For release builds, use your release keystore path and credentials. ## SDK Installation & Initialization The react-native-redirectly SDK is pure TypeScript. Install it via npm or yarn, then initialize with your API key at app startup. ### Install the SDK **npm or yarn** ```bash npm install react-native-redirectly # or yarn add react-native-redirectly ``` ### Initialize Redirectly Initialize the SDK at your app's entry point (App.tsx or index.js): **App.tsx** ```tsx import React, { useEffect } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { Redirectly } from 'react-native-redirectly'; import AsyncStorage from '@react-native-async-storage/async-storage'; import RootStack from './navigation/RootStack'; export default function App() { useEffect(() => { const initializeRedirectly = async () => { try { const redirectly = Redirectly.getInstance(); await redirectly.initialize({ apiKey: 'your-api-key', enableDebugLogging: __DEV__, asyncStorage: AsyncStorage, // Optional but recommended }); // Listen for install attribution redirectly.onAppInstalled((installData) => { if (installData.matched) { console.log( 'Install attributed to:', installData.matchedClick?.slug ); // Store or send to analytics } }); // Listen for incoming deep links redirectly.onDeepLink((deepLink) => { console.log('Deep link received:', deepLink); // Handle navigation - integrate with React Navigation }); } catch (error) { console.error('Failed to initialize Redirectly:', error); } }; initializeRedirectly(); }, []); return ( ); } ``` ### For Expo The SDK works the same in Expo. Install and initialize identically. For production Expo apps, configure deep linking in your app.json (see the Expo Configuration section below). ## React Navigation v6 Integration React Navigation v6 provides a powerful linking configuration system. You declare your deep link URL scheme and map paths to screen names. Deep links are automatically matched and your app navigates to the correct screen. ### Configure React Navigation Linking Create a linking configuration that maps your Redirectly subdomain to screen routes: **navigation/linking.ts** ```typescript import * as Linking from 'expo-linking'; const prefix = Linking.createURL('/'); export const linking = { prefixes: [ prefix, 'myapp.redirectly.app', 'https://myapp.redirectly.app', 'myapp://', ], config: { screens: { Home: '', Profile: 'profile/:userId', Product: 'product/:productId', Campaign: 'campaign/:campaignId', NotFound: '*', }, }, }; ``` Replace myapp with your Redirectly subdomain. Add more routes and parameters as needed. ### Use Linking in NavigationContainer Pass the linking configuration to your NavigationContainer: **App.tsx** ```tsx import React, { useEffect, useState, useRef } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { Redirectly } from 'react-native-redirectly'; import { linking } from './navigation/linking'; import RootStack from './navigation/RootStack'; export default function App() { const navigationRef = useRef(null); const [isReady, setIsReady] = useState(false); const [initialRoute, setInitialRoute] = useState(null); useEffect(() => { const initializeRedirectly = async () => { try { const redirectly = Redirectly.getInstance(); await redirectly.initialize({ apiKey: 'your-api-key', enableDebugLogging: __DEV__, }); // Handle deferred deep link on first app open redirectly.onAppInstalled((installData) => { if (installData.matched && installData.deferred?.url) { console.log('Deferred link:', installData.deferred.url); // Navigate to the deferred deep link if (navigationRef.current) { const state = navigationRef.current?.getRootState(); const url = installData.deferred.url; navigationRef.current?.navigate(...); // Or use linking.parse() } } }); setIsReady(true); } catch (error) { console.error('Failed to initialize Redirectly:', error); setIsReady(true); } }; initializeRedirectly(); }, []); if (!isReady) return null; return ( } > ); } ``` ## Deferred Deep Link Handling Deferred deep links are links that were clicked before the app was installed. The Redirectly SDK detects the install and delivers the original link data when the app opens for the first time. You listen for this via the onAppInstalled callback. ### Handle onAppInstalled Callback The onAppInstalled callback fires when the app is opened for the first time after installation, and if the install matched a Redirectly link click, it provides the deferred deep link data: **Deferred Link Handling** ```typescript const redirectly = Redirectly.getInstance(); redirectly.onAppInstalled(async (installData) => { console.log('Install Data:', installData); // installData structure: // { // matched: boolean, // matchedClick?: { // slug: string, // url: string, // timestamp: number, // utmParams?: { // source?: string, // medium?: string, // campaign?: string, // content?: string, // term?: string, // } // }, // deferred?: { // url: string, // customData?: Record, // } // } if (installData.matched) { console.log('Install matched a Redirectly click'); // Record attribution console.log('Campaign:', installData.matchedClick?.utmParams?.campaign); console.log('Source:', installData.matchedClick?.utmParams?.source); // Handle deferred deep link if (installData.deferred?.url) { console.log('Deferred Deep Link:', installData.deferred.url); // Navigate to the screen specified in the deep link // Parse the URL and navigate const { path, params } = parseDeepLink(installData.deferred.url); // For example: // If deferred URL is "https://myapp.redirectly.app/product/123" // Navigate to { name: 'Product', params: { productId: '123' } } } } }); function parseDeepLink(url: string) { // Simple example - your implementation may be more complex const parsed = new URL(url); const path = parsed.pathname; if (path.startsWith('/product/')) { const productId = path.split('/')[2]; return { screen: 'Product', params: { productId }, }; } if (path.startsWith('/profile/')) { const userId = path.split('/')[2]; return { screen: 'Profile', params: { userId }, }; } return { screen: 'Home', params: {} }; } ``` ### Attribution Data The installData includes UTM parameters and other attribution data. Use this for analytics and campaign tracking: **Attribution Example** ```typescript // Example: tracking with analytics service redirectly.onAppInstalled((installData) => { if (installData.matched && installData.matchedClick) { const { utmParams, timestamp } = installData.matchedClick; // Send to your analytics backend analytics.trackInstall({ source: utmParams?.source, medium: utmParams?.medium, campaign: utmParams?.campaign, timestamp, }); // Track in segment, mixpanel, firebase, etc. segment.track('App Installed', { utm_campaign: utmParams?.campaign, utm_source: utmParams?.source, matched: true, }); } }); ``` ## Testing & Debugging Test deep links on simulators, emulators, and real devices using platform-specific tools. Enable debug logging in the SDK to troubleshoot issues. ### iOS Testing (Simulator & Device) **Test via xcrun (Simulator)** ```bash xcrun simctl openurl booted "https://myapp.redirectly.app/product/123" ``` Replace myapp with your subdomain. **Test via Safari (Real Device)** Open Safari on your device and navigate to your Redirectly subdomain URL directly, or create an HTML page with a link to test. ### Android Testing (Emulator & Device) **Test via adb (Emulator & Device)** ```bash adb shell am start -a android.intent.action.VIEW -d "https://myapp.redirectly.app/product/123" ``` Replace myapp with your subdomain. **View adb logs** ```bash adb logcat | grep -i "redirectly" # or filter by tag adb logcat -s "RedirectlySDK" ``` ### Enable Debug Logging Set enableDebugLogging to true when initializing the SDK to see detailed logs: **Debug Mode** ```typescript const redirectly = Redirectly.getInstance(); await redirectly.initialize({ apiKey: 'your-api-key', enableDebugLogging: true, // Enable for development }); ``` ### Common Issues - **iOS: Links not opening app** — Check Associated Domains in Signing & Capabilities. Verify AASA file using the AASA validator tool. - **Android: Links not opening app** — Verify intent filter autoVerify attribute is present. Check assetlinks.json with the assetlinks validator. - **Deep link data not received** — Enable debug logging and check console output. Ensure your linking config matches your URL structure. - **Deferred link data missing** — Ensure AsyncStorage is configured for install tracking. Uninstall and reinstall the app to test first-open. ## Expo Configuration Redirectly works seamlessly with Expo. The SDK is pure TypeScript, so it works out of the box. For production Expo apps, configure deep linking in your app.json so the Expo build system registers your deep link handler. ### Configure app.json Add the deep linking configuration to your Expo app.json to register your Redirectly subdomain: **app.json** ```json { "expo": { "scheme": "myapp", "plugins": [ [ "expo-build-properties", { "ios": { "useFrameworks": "static" } } ] ], "ios": { "associatedDomains": [ "applinks:myapp.redirectly.app", "webcredentials:myapp.redirectly.app" ] }, "android": { "intentFilters": [ { "action": "VIEW", "autoVerify": true, "data": [ { "scheme": "https", "host": "*.redirectly.app" } ], "category": ["BROWSABLE", "DEFAULT"] } ] } } } ``` Replace myapp with your Redirectly subdomain. ### EAS Build for Production For production Expo apps, use EAS Build to build your app with the correct certificates and entitlements: **Build Command** ```bash eas build --platform ios --profile production eas build --platform android --profile production ``` ### Expo Go Limitations Expo Go (the preview app) does not support custom URL schemes or deep linking from your subdomain. For testing deep links in development, use a custom dev client or EAS Build. ## Migration from Firebase Dynamic Links If you're currently using Firebase Dynamic Links (FDL), Redirectly offers a modern, simpler alternative with comparable functionality. The migration involves updating your SDK initialization and link handling code. ### API Comparison **Firebase vs Redirectly** ```typescript // Firebase Dynamic Links const link = await dynamicLinks().getInitialLink(); if (link) { handleDeepLink(link.url); } dynamicLinks().onLink((link) => { handleDeepLink(link.url); }); // Redirectly const redirectly = Redirectly.getInstance(); await redirectly.initialize({ apiKey: 'your-key' }); redirectly.onAppInstalled((installData) => { if (installData.deferred?.url) { handleDeepLink(installData.deferred.url); } }); ``` For a detailed step-by-step migration guide with code examples and best practices, see our [Firebase Dynamic Links alternative](https://redirectly.app/firebase-dynamic-links-alternative.md) page. Also check out the [deep linking guide](https://redirectly.app/deep-linking-guide.md) and [deferred deep linking](https://redirectly.app/deferred-deep-linking.md) concepts. ## Frequently Asked Questions ### What is deferred deep linking? Deferred deep linking allows links to work even if the app is not installed. When a user clicks a Redirectly link and doesn't have the app, they're directed to the app store. After installing, the app opens and the SDK retrieves the original link data, routing the user to the correct screen. This is critical for acquisition campaigns. ### Does Redirectly work with Expo? Yes, Redirectly works with both Expo and bare React Native. For Expo, configure deep linking in app.json and use EAS Build for production. The SDK is pure TypeScript and works out of the box. ### What is install attribution? Install attribution matches app installs to specific link clicks or campaigns. When a user installs the app after clicking a Redirectly link, the onAppInstalled callback provides details about which campaign, source, or medium drove the install. You can use this data for analytics and campaign optimization. ### Do I need to write native code? For bare React Native, you need to configure iOS and Android native deep link settings (entitlements, AndroidManifest.xml, AppDelegate). However, the Redirectly SDK itself is pure TypeScript with no native module code. For Expo, you configure deep linking in app.json. ### How do I test deep links in the simulator/emulator? For iOS Simulator, use xcrun simctl openurl. For Android Emulator, use adb shell am start. See the Testing & Debugging section for detailed commands and examples. ### Can I migrate from Firebase Dynamic Links? Yes. Redirectly provides similar functionality with a comparable API. See our Firebase Dynamic Links alternative page for step-by-step migration instructions and code comparisons. ## Related Have questions? Check out the [React Native SDK docs](https://docs.redirectly.app/react-native) or explore related guides: - [AASA Validator Tool](https://redirectly.app/aasa-validator.md) — Verify your iOS apple-app-site-association file - [assetlinks Validator Tool](https://redirectly.app/assetlinks-validator.md) — Verify your Android assetlinks.json file - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) — Deep linking for Flutter apps - [Firebase Dynamic Links Alternative](https://redirectly.app/firebase-dynamic-links-alternative.md) — Migration guide and comparison - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) — Core deep linking concepts - [Deferred Deep Linking](https://redirectly.app/deferred-deep-linking.md) — Detailed deferred linking information --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Firebase Dynamic Links Alternative > Firebase Dynamic Links is deprecated. Redirectly is the modern alternative: deferred deep linking, install attribution, and Flutter & RN SDKs. Migrate fast. - Canonical: https://redirectly.app/firebase-dynamic-links-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Firebase Dynamic Links was deprecated. Redirectly gives you deferred deep linking, install attribution, and first-class Flutter and React Native SDKs—without the lock-in. Migrate in minutes. ## Why Firebase Dynamic Links ended Google announced the deprecation of Firebase Dynamic Links (FDL) in 2022, with support ending in August 2025. For years, FDL was the go-to solution for deferred deep linking and install attribution on iOS and Android. When it was shut down, thousands of mobile teams—especially those using Flutter and React Native—were left without a built-in way to create links that survive app installation. Firebase Dynamic Links handled redirects to the App Store or Play Store, stored the intended destination, and matched the user back to that destination after install using device fingerprinting. Replacing that behavior requires a service that does the same three things: redirect, store, and match. Redirectly was built specifically to fill that gap. If you are still on Firebase Dynamic Links, you need to migrate before the shutdown. Redirectly offers a drop-in style replacement with Flutter and React Native SDKs, so you can keep your existing flow and swap the backend. ## What you need in a replacement A solid Firebase Dynamic Links alternative should give you deferred deep linking, install attribution, and a simple way to create and manage links. It should work with your stack—Flutter, React Native, or native iOS/Android—and scale with your traffic without locking you into a single vendor. - **Deferred deep linking** — Links that survive app install and route users to the right screen or campaign. - **Install attribution** — Match installs to the original link click so you know which campaigns drive installs. - **SDKs for your stack** — Flutter and React Native SDKs that work without heavy native code changes. ## Redirectly vs Firebase Dynamic Links Side-by-side, Redirectly covers the same core use cases as FDL and adds clearer pricing, real-time analytics, and first-class Flutter and React Native support. | Feature | Firebase Dynamic Links | Redirectly | | --- | --- | --- | | Deferred deep linking | Yes | Yes | | Install attribution | Yes | Yes | | Status | Deprecated (Aug 2025) | Active, maintained | | Flutter SDK | Legacy | Pure Dart, no native code | | React Native SDK | Limited | TypeScript, Expo & bare RN | | Custom subdomains | Via Firebase | Included (e.g. yourapp.redirectly.app) | | Real-time analytics | Limited | Dashboard + API | | Free tier | N/A | Yes | ## How migration works Migrating from Firebase Dynamic Links to Redirectly follows a simple path: create a Redirectly project, configure your subdomain and native deep link setup (Universal Links / App Links), then swap the SDK. Your existing link flows—email, ads, referrals—can keep the same structure; only the backend and SDK calls change. 1. **Create a Redirectly project** — Sign up at redirectly.app, create a project, and get your API key and subdomain (e.g. myapp.redirectly.app). 2. **Configure iOS and Android** — Add your subdomain to Associated Domains (iOS) and intent filters (Android) so the OS routes your links to your app. 3. **Replace the SDK** — Remove the Firebase Dynamic Links SDK and add the Flutter or React Native Redirectly SDK. Initialize with your API key and handle incoming links and install attribution in your app. 4. **Create links via API or dashboard** — Use the Redirectly API or dashboard to create links. Point existing campaigns to the new links and test the full flow. For a step-by-step Flutter migration with code samples, see our [migration guide](https://redirectly.app/blog/migrating-from-firebase-dynamic-links.md). **Flutter example** ```dart await redirectly.initialize(RedirectlyConfig( apiKey: 'your-api-key', debug: true, )); ``` **React Native / TypeScript example** ```typescript import Redirectly from 'react-native-redirectly'; const redirectly = Redirectly.getInstance(); await redirectly.initialize({ apiKey: 'your-api-key', enableDebugLogging: __DEV__, }); redirectly.onAppInstalled((install) => { if (install.matched) { navigation.navigate(install.matchedClick?.slug); } }); ``` For a step-by-step migration guide with additional code samples and troubleshooting, see our [comprehensive migration guide](https://redirectly.app/blog/migrating-from-firebase-dynamic-links.md). You can also check out our platform-specific documentation for [Flutter deferred deep linking](https://redirectly.app/flutter-deferred-deep-linking.md) and [React Native deferred deep linking](https://redirectly.app/react-native-deferred-deep-linking.md). **Migration validation tools.** When configuring Universal Links and App Links, use our validation tools to ensure everything is set up correctly: - [AASA Validator](https://redirectly.app/aasa-validator.md) — Validate your Apple App Site Association configuration for iOS - [Asset Links Validator](https://redirectly.app/assetlinks-validator.md) — Validate your Asset Links configuration for Android ## Why teams choose Redirectly Redirectly was built as a Firebase Dynamic Links alternative from day one. We focus on deferred deep linking and install attribution so you get a simple, reliable service without the overhead of a full growth platform. - Deferred deep linking that works the same way as FDL: link → store → install → match → route. - Install attribution so you can see which links and campaigns drive installs. - Flutter SDK (pure Dart) and React Native SDK (TypeScript) with no native code required for basic setup. - Custom subdomains and Universal Links / App Links support out of the box. - Real-time analytics and a straightforward API for creating and managing links. - Free tier and transparent pricing so you can start small and scale. ## Frequently asked questions Have questions about migrating from Firebase Dynamic Links or using Redirectly? Here are some common answers. ### Why were Firebase Dynamic Links deprecated? Google announced the deprecation of Firebase Dynamic Links in 2022, with support ending in August 2025. This was part of their strategic shift in their product roadmap. Teams relying on FDL need to migrate to an alternative solution like Redirectly. ### What's the best replacement for Firebase Dynamic Links? Redirectly is a modern Firebase Dynamic Links alternative built specifically for deferred deep linking and install attribution. It offers native SDKs for Flutter and React Native, custom subdomains, real-time analytics, and transparent pricing without vendor lock-in. ### How do I migrate from Firebase Dynamic Links to Redirectly? Migration is straightforward: create a Redirectly project, configure your subdomain and native deep link setup (Universal Links / App Links), then replace the SDK in your app. Most teams can complete the migration in minutes to hours. Your existing link flows can keep the same structure. ### Does Redirectly support both Flutter and React Native? Yes. Redirectly provides a pure Dart SDK for Flutter and a TypeScript SDK for React Native. Both support deferred deep linking, install attribution, and work with Expo and bare React Native projects without requiring heavy native code changes. ### Is Redirectly free? Yes, Redirectly offers a free tier to get started. You can create projects, configure links, and handle basic traffic without any cost. Premium plans are available for teams needing higher volume and advanced analytics features. ### How long does migration take? Most teams can migrate from Firebase Dynamic Links to Redirectly in minutes to hours, depending on your app complexity. The migration involves creating a new project, updating your native configuration files, and replacing the SDK initialization code. ## Getting started Create a free account, get your API key and subdomain, and follow the Flutter or React Native docs to integrate. If you are migrating from Firebase Dynamic Links, use the migration guide for a detailed walkthrough and code comparisons. ### Compare alternatives Evaluating other options? Check out how Redirectly compares to other popular deep linking solutions: - [Redirectly vs Branch](https://redirectly.app/branch-alternative.md) - [Redirectly vs AppsFlyer](https://redirectly.app/apps-flyer-alternative.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly vs Branch > Redirectly vs Branch: compare deferred deep linking, install attribution, Flutter SDKs, and pricing. Pure Dart SDKs and self-serve signup, no complexity. - Canonical: https://redirectly.app/branch-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Comprehensive comparison of Redirectly and Branch: pricing, Flutter SDK support, feature parity, and migration guide. Learn why developers choose Redirectly for simpler, more transparent deferred deep linking. ## Why developers switch from Branch Branch is powerful but built for enterprise marketing teams. If you're a mobile developer needing pure deferred deep linking and install attribution, you often hit these pain points: ### Branch challenges - **Opaque pricing:** Enterprise-only, requires sales contact, typically $2,000+/month minimum - **Complexity:** Full platform when you just need deep linking—lots to learn - **Flutter support limited:** Flutter SDK requires native Kotlin/Swift code for some features - **Self-serve signup unavailable:** All customers route through enterprise sales process - **3rd-party tracking:** Core analytics rely on 3rd-party SDKs and data collection ### Redirectly advantages - **Transparent pricing:** Starter $24/mo, Pro $80/mo—no surprises - **Focused simplicity:** One job done well: deferred deep linking & attribution - **Pure Dart SDK:** Full Flutter support without any native code required - **Self-serve signup:** Get started in minutes with API key and subdomain - **Privacy-first:** No 3rd-party tracking—your data stays yours ## Feature comparison table Both Redirectly and Branch handle core deep linking and attribution. The differences emerge in SDK support, deployment model, and pricing structure. | Feature | Redirectly | Branch | | --- | --- | --- | | Deferred deep linking | Yes | Yes | | Install attribution | Yes | Yes | | Flutter SDK (pure Dart, no native code) | Yes | No | | React Native SDK | Yes | Yes | | Custom subdomains | Yes | Yes | | AASA/assetlinks hosting | Yes | Yes | | MCP Server for AI editors | Yes | No | | Privacy-first (no 3rd-party tracking) | Yes | No | | Real-time analytics dashboard | Yes | Yes | | Free tier available | Yes | No | | Self-serve signup | Yes | No | ## Pricing comparison Redirectly offers transparent, self-serve pricing with no surprises. Branch operates on enterprise sales—you won't see pricing until you talk to a representative. ### Redirectly Transparent, self-serve pricing. Pay only for what you use. **Free — $0** - 10,000 monthly links - 1 subdomain - Basic analytics - Community support **Starter — $24/month** - 100,000 monthly links - 5 subdomains - Advanced analytics - Email support **Pro — $80/month** - Unlimited links - Unlimited subdomains - Real-time dashboards - Priority support - Webhook callbacks ### Branch Enterprise pricing. Requires sales contact. No public pricing. **Starter / Pro** — Not available for self-serve signup. No public pricing. Contact sales. **Enterprise** — Typical starting point for most customers: $2,000+ /month. Actual pricing varies based on: - Monthly event volume - Required integrations - Support tier - Negotiation leverage **What's included:** Full Branch platform, A/B testing, content management, advanced attribution, dedicated support. ### Pricing savings: Redirectly vs Branch If you only need deferred deep linking and install attribution: $80/mo (Redirectly Pro) vs $2,000+/mo (Branch Enterprise). Save up to $23,040/year with Redirectly. ## Developer experience comparison ### Setup & Integration **Redirectly:** Pure Dart SDK for Flutter means no native code required. Get an API key in minutes. Full setup in under 10 minutes for most apps. **Branch:** iOS setup requires Xcode, Swift, and manual AASA configuration. Android setup requires Gradle and native manifest updates. Kotlin code integration needed for some features. ### Flutter Support **Redirectly:** Full-featured Flutter package. Pure Dart implementation. No native code needed. Works with go_router out of the box. **Branch:** Flutter support available but requires native platform channels. Some features need Kotlin/Swift implementation. More complex integration for Flutter developers. ### Documentation & Support **Redirectly:** Developer-focused docs with Flutter and React Native examples. Active GitHub discussions. Community Slack. Email support on paid plans. **Branch:** Comprehensive documentation but oriented toward marketing teams. Developer docs scattered across platform. Enterprise sales support model means slower response to technical questions. ### API & Dashboard **Redirectly:** Simple REST API. Lightweight dashboard focused on link creation and analytics. Real-time metrics. MCP Server integration for AI-assisted development. **Branch:** Feature-rich API but steeper learning curve. Complex dashboard with many features you might not need. Requires familiarity with Branch's data model. ## Migration guide from Branch SDK Migrating from Branch to Redirectly is straightforward. Both SDKs handle deep linking and attribution similarly. You can run both simultaneously during migration to test thoroughly before fully switching. ### Step 1: Update pubspec.yaml **Branch** ```yaml dependencies: flutter_branch_sdk: ^5.0.0 ``` **Redirectly** ```yaml dependencies: redirectly: ^1.0.0 ``` ### Step 2: Initialization **Branch initialization** ```dart import 'package:flutter_branch_sdk/flutter_branch_sdk.dart'; void initState() { final branchInstance = FlutterBranchSdk.instance; branchInstance.initSession( onData: (data) { print('Branch: ' + data.toString()); }, onError: (error) { print('Branch error: ' + error); }, ); } ``` **Redirectly initialization** ```dart import 'package:redirectly/redirectly.dart'; void main() async { await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Listen to deep link events Redirectly.onDeepLink.listen((link) { print('Deep link: ' + link.path); }); runApp(const MyApp()); } ``` ### Step 3: Handling deep links **Branch deep link handling** ```dart BranchDeepLinkData data = await FlutterBranchSdk .instance.getFirstReferringParams(); if (data.success) { String? route = data.jsonMap['~route'] as String?; // Navigate based on route } ``` **Redirectly deep link handling** ```dart // With go_router Redirectly.onDeepLink.listen((link) { if (link.path == 'product/123') { context.go('/product/123'); } }); // Or access directly final params = await Redirectly.getInitialLink(); if (params != null) { print('Path: ' + params.path); } ``` ### Migration strategy 1. **Add Redirectly SDK** to pubspec.yaml alongside Branch. Run both during testing. 2. **Initialize both SDKs** in your main.dart. Test that deep links work through Redirectly. 3. **Run QA testing** with deep links from Redirectly dashboard. Verify attribution tracking works. 4. **Deploy** new version with both SDKs active (no downtime). 5. **Monitor analytics** for 1-2 weeks. Watch both dashboards to ensure parity. 6. **Remove Branch SDK** from pubspec.yaml. Deploy final version. ## Frequently asked questions ### Is Redirectly a complete Branch replacement? Redirectly replaces Branch for deferred deep linking and install attribution—the core use case. If you need Branch's A/B testing, content management, or full attribution stacks, you can use Redirectly for linking and complement it with other tools. ### Does Redirectly support Universal Links and App Links? Yes. Redirectly handles AASA (Apple App Site Association) hosting for iOS and assetlinks.json for Android. Custom subdomains are supported. We validate your configuration and provide tools to test. ### How does Redirectly pricing compare to Branch? Redirectly: Starter $24/mo, Pro $80/mo with transparent, self-serve pricing. Branch: Enterprise-only, typically $2,000+/mo. Redirectly is 95% cheaper for most use cases. ### Can I migrate from Branch without downtime? Yes. Run both SDKs simultaneously. Update your pubspec.yaml, initialize both, test thoroughly, then remove Branch. Users won't experience any disruption. ### Does Redirectly work with go_router? Yes. Redirectly provides deep link URLs that integrate seamlessly with go_router route guards and navigation stack. Listen to Redirectly.onDeepLink and pass the path to context.go(). ### What if I have questions during migration? Redirectly provides email support on Starter and Pro plans, plus community Slack and GitHub discussions. We're here to help your team transition smoothly. ## Related See how Redirectly compares to other mobile linking platforms: - [Firebase Dynamic Links Alternative](https://redirectly.app/firebase-dynamic-links-alternative.md) - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [AASA Validator Tool](https://redirectly.app/aasa-validator.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly vs AppsFlyer > AppsFlyer too expensive? Redirectly is the best AppsFlyer alternative for small teams. Compare deep linking, Flutter SDKs, and pricing. Start free today. - Canonical: https://redirectly.app/apps-flyer-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native AppsFlyer is expensive, marketing-focused, and lacks native Flutter support. Redirectly is built for developers who need deferred deep linking and install attribution. Pure Dart SDK, simple pricing, and docs written for engineers. ## Why developers look for AppsFlyer alternatives AppsFlyer is a powerful mobile measurement partner (MMP) with advanced attribution, fraud detection, and SKAdNetwork support. However, it comes with significant trade-offs for development teams, particularly those building with Flutter or React Native. ### Enterprise pricing model Limited free tier. Beyond that, AppsFlyer requires enterprise contracts and custom quotes. No transparent self-serve pricing for growth-stage teams. ### Marketing-team focus AppsFlyer docs, SDKs, and features emphasize campaign tracking and analytics. Documentation is written for product managers and marketers, not engineers building mobile apps. ### No native Flutter support AppsFlyer Flutter integration relies on native wrappers, not pure Dart. This adds complexity and requires native code understanding for iOS and Android. ### Complex setup AppsFlyer integration requires native configuration, multiple SDKs, and deep understanding of iOS and Android app attribution flows. Setup can take weeks. If you need the full AppsFlyer feature set (SKAdNetwork, fraud detection, full MMP), AppsFlyer is the right choice. But if you simply need deferred deep linking and install attribution, you're paying for and maintaining complexity you don't need. ## Feature comparison table Head-to-head comparison of Redirectly vs AppsFlyer. Focus is on core deep linking and attribution features that matter to development teams. | Feature | Redirectly | AppsFlyer | Notes | | --- | --- | --- | --- | | Deferred deep linking | Yes | Yes | Both platforms excel here. Core feature. | | Install attribution | Yes | Yes | Both support basic and advanced attribution. | | Flutter SDK (pure Dart) | Yes | No | Redirectly: pure Dart. AppsFlyer: native wrappers only. | | React Native SDK | Yes | Yes | Both have TypeScript-friendly SDKs. | | Developer-focused documentation | Yes | No | Redirectly: code-first docs. AppsFlyer: marketing-focused. | | go_router / routing examples | Yes | No | Redirectly: full routing integration guides. AppsFlyer: basic integration only. | | MCP Server for AI editors | Yes | No | Redirectly: built-in AI integration. AppsFlyer: not available. | | Privacy-first by design | Yes | No | Redirectly: minimal integrations. AppsFlyer: extensive 3rd-party integrations. | | Self-serve transparent pricing | Yes | No | Redirectly: publicly listed pricing tiers. AppsFlyer: enterprise quotes only. | | Custom subdomains | Yes | Yes | Both support branded link domains. | | SKAdNetwork support | No | Yes | AppsFlyer: full SKAdNetwork implementation. Redirectly: coming soon. | **Summary:** Both platforms support the core deep linking and attribution use cases. Redirectly wins on developer experience (pure Dart, routing examples, AI integration) and transparent pricing. AppsFlyer wins on advanced iOS features (SKAdNetwork) and full MMP capabilities. ## Pricing comparison AppsFlyer uses an enterprise licensing model with limited transparency. Redirectly offers clear, self-serve pricing tiers designed for growth-stage teams and developers. ### Redirectly Transparent, self-serve pricing. - **Free** — $0 / month. 100 clicks/month, basic dashboard, test environment. - **Starter** — $24 / month. 10,000 clicks/month, subdomain, analytics, API access. - **Pro** — $80 / month. 100,000 clicks/month, team support, advanced analytics, priority support. No hidden fees. Upgrade or downgrade anytime. ### AppsFlyer Enterprise licensing model. - **Free Tier** — Limited free access. Very limited features and attribution data retention. - **Growth** — Custom quote. Typically $500+/month depending on volume and features. - **Enterprise** — Custom quote. Minimum contract required. Complex pricing based on MAU and features. No public pricing. Enterprise contracts required. ### Why pricing matters AppsFlyer's enterprise licensing makes it difficult for small teams and startups to try the platform or budget accurately. Redirectly's transparent pricing lets you start free and grow predictably. You only pay for what you use, with no surprises. ## Developer experience AppsFlyer is designed for product managers and marketers. Redirectly is built for developers. This difference shows up in SDKs, documentation, and integration patterns. ### SDK Design **Redirectly:** - Pure Dart for Flutter—no native wrappers - TypeScript SDK for React Native - go_router integration examples - Works with state management (Riverpod, Provider, GetX) **AppsFlyer:** - Native wrappers for Flutter (requires native code) - React Native SDK available - No routing framework examples - Designed for campaign attribution, not routing ### Documentation AppsFlyer documentation targets product teams and focuses on campaign creation, audience segmentation, and analytics dashboards. You won't find: - Dart code examples for Flutter - pubspec.yaml integration patterns - go_router or Riverpod integration guides - Deep link data flow architecture - Testing deep links in development Redirectly documentation is written by engineers, for engineers. Every feature comes with Dart code, complete examples, and integration guides for popular Flutter packages. ### Setup complexity **Redirectly** — Time to first deep link: 10 minutes. Add package, initialize in main.dart, handle deep links. Pure Dart, no native configuration needed for basic setup. **AppsFlyer** — Time to first attribution: 1–2 weeks. Configure iOS and Android native code, set up tracking endpoints, configure server-to-server callbacks. Requires iOS/Android expertise. ## Migration guide from AppsFlyer SDK Migrating from AppsFlyer to Redirectly is straightforward. Both platforms provide similar APIs for deep linking and attribution. Here's what a typical migration looks like. ### Step 1: Update pubspec.yaml Replace AppsFlyer with Redirectly in your Flutter project. AppsFlyer: ```yaml dependencies: appsflyer_sdk: ^6.0.0 # AppsFlyer requires separate # Android/iOS configuration ``` Redirectly: ```yaml dependencies: redirectly: ^1.0.0 # That's it! Pure Dart, # no native setup needed ``` ### Step 2: Initialize the SDK Update your main.dart initialization code. AppsFlyer: ```dart import 'package:appsflyer_sdk/appsflyer_sdk.dart'; void main() { AppsflyerSdk appsflyerSdk = AppsflyerSdk({ "afDevKey": "YOUR_APPSFLYER_DEV_KEY", "appId": "YOUR_APP_ID", "isDebug": true, }); appsflyerSdk.initSdk(); runApp(MyApp()); } ``` Redirectly: ```dart import 'package:redirectly/redirectly.dart'; void main() { Redirectly.initialize( apiKey: 'YOUR_REDIRECTLY_API_KEY', subdomain: 'your-subdomain', ); runApp(MyApp()); } ``` ### Step 3: Handle deep links Both platforms provide similar deep link handling. Update your app initialization. AppsFlyer: ```dart appsflyerSdk.onAppOpenAttribution( (res) { var deeplink = res["deep_link_value"]; // Navigate based on deeplink } ).onError((error) { print(error); }); ``` Redirectly: ```dart Redirectly.onDeepLink((deepLink) { // deepLink contains the full // deep link data from the click navigateToDeepLink(deepLink); }).onError((error) { print(error); }); ``` ### Step 4: Create links programmatically (optional) If you create deep links in code, update the API call. AppsFlyer: ```dart appsflyerSdk.generateUniversalLink( parameters: { "campaign": "summer_sale", "product_id": "widget_123", } ).then((deeplink) { print(deeplink); }); ``` Redirectly: ```dart final deepLink = Redirectly.createLink( campaign: 'summer_sale', data: { 'product_id': 'widget_123', }, ); print(deepLink.url); ``` ### Migration complete That's the basic migration. You've replaced AppsFlyer SDK with Redirectly and updated the initialization and deep link handling code. Both platforms provide similar attribution data, so your analytics dashboards will continue to work. For full migration guides including go_router integration, team setup, and advanced features, see the [Redirectly documentation](https://docs.redirectly.app). ## Frequently asked questions ### Is Redirectly a complete AppsFlyer replacement? Redirectly focuses on deferred deep linking and install attribution—the core features most teams actually use. It is not a complete MMP (Mobile Measurement Partner) replacement for AppsFlyer's SKAdNetwork, fraud detection, and full attribution suite. However, if you only need deep linking and basic attribution, Redirectly is simpler and more affordable. ### Does Redirectly have a pure Dart SDK for Flutter? Yes. Redirectly provides a pure Dart SDK for Flutter with no native code required. AppsFlyer only offers native wrappers (platform channels), which means you need to understand iOS and Android native code to integrate it properly. Redirectly is designed for Flutter developers first. ### Can I migrate from AppsFlyer to Redirectly easily? Yes. Both platforms provide similar deep linking and attribution APIs. Migration involves: (1) replacing the AppsFlyer package with Redirectly in pubspec.yaml, (2) updating initialization code, and (3) adjusting deep link handling. Most teams complete migration in a few hours. ### How much cheaper is Redirectly compared to AppsFlyer? Redirectly starts at $24/month (Starter) and $80/month (Pro). AppsFlyer has a limited free tier and then requires enterprise quotes for higher volumes. For small to mid-size teams, Redirectly is typically 5–10x less expensive. You get transparent pricing with no hidden costs or surprise bills. ### Why is AppsFlyer documentation not developer-focused? AppsFlyer is designed for marketing and product teams. Their documentation emphasizes campaign tracking, audience segmentation, and analytics dashboards. Redirectly docs are written by engineers for engineers: every feature comes with Dart code, pubspec.yaml examples, and go_router integration guides. ### Does Redirectly support SKAdNetwork? Not currently. SKAdNetwork is an Apple attribution framework that AppsFlyer fully supports. If SKAdNetwork attribution is critical to your iOS workflow, AppsFlyer is the better choice. For most teams focused on deep linking and basic attribution, Redirectly's approach is sufficient. SKAdNetwork support is on the Redirectly roadmap. ## Related - [Firebase Dynamic Links Alternative](https://redirectly.app/firebase-dynamic-links-alternative.md) — Compare Redirectly to Firebase for deep linking. - [Branch.io Alternative](https://redirectly.app/branch-alternative.md) — Redirectly vs Branch for mobile deep linking. - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Guide to deferred deep linking in Flutter apps. - [AASA Validator](https://redirectly.app/aasa-validator.md) — Debug Apple App Site Association for iOS deep linking. - [Assetlinks Validator](https://redirectly.app/assetlinks-validator.md) — Validate Android deep linking configuration. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Adjust Alternative > Redirectly vs Adjust: compare mobile attribution, deep linking, Flutter SDKs, and pricing. Developer-friendly tools and clear pricing, no enterprise bloat. - Canonical: https://redirectly.app/adjust-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Comprehensive comparison of Redirectly and Adjust: pricing, Flutter SDK support, feature parity, developer experience, and migration guide. Learn why developers choose Redirectly for simpler, transparent mobile attribution. ## Why developers seek Adjust alternatives Adjust is powerful but built for enterprise marketing teams and agencies. If you're a mobile developer needing straightforward mobile attribution and deep linking, you often hit these pain points: **Adjust challenges:** - **Enterprise pricing only:** No self-serve pricing, requires sales contact, custom quotes per customer - **Marketing team focused:** Dashboard and documentation oriented toward marketing, not developers - **Flutter support limited:** No pure Dart SDK—requires native platform channels and Kotlin/Swift code - **Complex integration:** SKAdNetwork setup, multiple configuration steps, steep learning curve for attribution model - **3rd-party tracking:** Core analytics rely on third-party SDKs and data collection networks **Redirectly advantages:** - **Transparent pricing:** Free tier, Starter $24/mo, Pro $80/mo—self-serve, no sales calls - **Developer-friendly:** Documentation and tools designed for mobile developers, not marketers - **Pure Dart SDK:** Full Flutter support without any native code required - **Simple setup:** Minimal configuration, clear deep linking model, quick integration - **Privacy-first:** No third-party tracking—your user data stays yours ## Feature comparison table Both Redirectly and Adjust handle mobile attribution and deep linking. The differences emerge in SDK support, deployment model, pricing structure, and specialized features. | Feature | Redirectly | Adjust | | --- | --- | --- | | Deep linking (Universal Links / App Links) | Yes | Yes | | Install attribution | Yes | Yes | | Flutter SDK (pure Dart, no native code) | Yes | No | | React Native SDK | Yes | Yes | | Developer-focused documentation | Yes | No | | MCP Server for AI editors | Yes | No | | Privacy-first (no 3rd-party tracking) | Yes | No | | Self-serve pricing | Yes | No | | Custom subdomains | Yes | Yes | | SKAdNetwork integration | No | Yes | | Fraud prevention & bot detection | No | Yes | ## Pricing comparison Redirectly offers transparent, self-serve pricing with no surprises. Adjust operates on enterprise sales—pricing varies by customer and requires negotiation with their sales team. ### Redirectly Transparent, self-serve pricing. Pay only for what you use. - **Free** — $0. 10,000 monthly links, 1 subdomain, basic attribution, community support. - **Starter** — $24/month. 100,000 monthly links, 5 subdomains, real-time analytics, email support. - **Pro** — $80/month. Unlimited links, unlimited subdomains, advanced dashboards, priority support, webhook callbacks. ### Adjust Custom enterprise pricing. Requires sales contact. Volume-based. - **Standard / Advanced** — Not available for self-serve signup. Pricing based on event volume. Contact sales. - **Enterprise** — Custom pricing based on volume and requirements. $500+ /month. Actual pricing varies based on: monthly event volume, number of apps, fraud prevention tier, support level, negotiation terms. What's included: mobile attribution, deep linking, SKAdNetwork integration, fraud prevention, advanced cohort analysis, dedicated support. ### Pricing savings: Redirectly vs Adjust If you only need mobile attribution and deep linking: $80/mo (Redirectly Pro) vs $500+/mo (Adjust Enterprise). Save up to $5,040/year with Redirectly. ## Developer experience comparison ### Setup & Integration **Redirectly:** Pure Dart SDK for Flutter means no native code required. Get an API key in minutes. Full setup in under 10 minutes for most apps. **Adjust:** iOS setup requires Xcode, Swift, and framework integration. Android setup requires Gradle and manifest updates. SDK size and initialization time can impact app performance. ### Flutter Support **Redirectly:** Full-featured Flutter package. Pure Dart implementation. No native code needed. Works seamlessly with go_router and standard Flutter navigation. **Adjust:** Flutter support available but requires native platform channels. Features dependent on native implementation. More complex setup for pure Flutter developers. ### Documentation & Support **Redirectly:** Developer-focused docs with Flutter and React Native examples. Active community. Clear integration guides. Email support on paid plans. **Adjust:** Comprehensive documentation but oriented toward marketing teams and analytics. Developer docs scattered across platform. Enterprise support model means slower response to technical questions. ### Attribution Model **Redirectly:** Simple, straightforward attribution. Deep link-based tracking. Real-time visibility. No complex configuration needed. **Adjust:** Advanced attribution model with SKAdNetwork, fraud prevention, and cohort analysis. More powerful but requires deeper understanding of attribution mechanics and privacy frameworks. ## Migration guide from Adjust SDK Migrating from Adjust to Redirectly is straightforward. Both SDKs handle mobile attribution and deep linking. You can run both simultaneously during migration to test thoroughly before fully switching. ### Step 1: Update pubspec.yaml Adjust: ```yaml dependencies: adjust_sdk: ^4.38.0 ``` Redirectly: ```yaml dependencies: redirectly: ^1.0.0 ``` ### Step 2: Initialization Adjust initialization: ```dart import 'package:adjust_sdk/adjust.dart'; void initState() { AdjustConfig config = AdjustConfig('your_app_token'); config.logLevel = AdjustLogLevel.verbose; Adjust.start(config); Adjust.setAttributionCallback( (attribution) { print(attribution.toString()); }, ); } ``` Redirectly initialization: ```dart import 'package:redirectly/redirectly.dart'; void main() async { await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Listen to attribution events Redirectly.onAttribution.listen( (attribution) { print('Attribution: ' + attribution.toString()); }, ); runApp(const MyApp()); } ``` ### Step 3: Tracking deep links Adjust deep link tracking: ```dart void onDeepLinking( String? deeplink) { Adjust.sendEvent(AdjustEvent( 'deep_link_click', )); if (deeplink != null) { // Navigate to route _navigateToDeeplink( deeplink, ); } } ``` Redirectly deep link tracking: ```dart // With go_router Redirectly.onDeepLink .listen((link) { context.go(link.path); }); // Or access directly final params = await Redirectly .getInitialLink(); if (params != null) { print('Path: ' + params.path); } ``` ### Migration strategy 1. **Add Redirectly SDK** to pubspec.yaml alongside Adjust. Run both during testing. 2. **Initialize both SDKs** in your main.dart. Configure both to listen to deep links. 3. **Run QA testing** with deep links and attribution events from both dashboards. 4. **Deploy** new version with both SDKs active (zero downtime for users). 5. **Monitor analytics** for 1-2 weeks. Compare attribution data in both dashboards. 6. **Remove Adjust SDK** from pubspec.yaml. Deploy final version. ## Frequently asked questions ### Is Redirectly a complete Adjust replacement? Redirectly replaces Adjust for deep linking and install attribution. If you need Adjust's SKAdNetwork integration, fraud prevention, or advanced cohort analysis, you can use Redirectly for attribution and supplement with other specialized tools. ### Does Redirectly support mobile attribution like Adjust? Yes. Redirectly tracks app installs, reinstalls, and user attribution through deep links. You get real-time attribution data without relying on SKAdNetwork or third-party tracking networks. ### How does Redirectly pricing compare to Adjust? Redirectly: Free tier, Starter $24/mo, Pro $80/mo with transparent self-serve pricing. Adjust: Enterprise-only with custom pricing, typically starting at $500+/mo. Redirectly is significantly more affordable for most startups and indie developers. ### Can I migrate from Adjust without downtime? Yes. Run both SDKs simultaneously. Update your pubspec.yaml, initialize both, test thoroughly, then remove Adjust. Users won't experience any disruption. ### Does Redirectly work with Flutter apps? Yes. Redirectly provides a pure Dart SDK with no native code required. Full Flutter support for deep linking and attribution, with seamless integration with go_router. ### What about SKAdNetwork and fraud prevention? Redirectly focuses on privacy-first attribution without third-party tracking. SKAdNetwork and fraud prevention are on the roadmap. For now, use Redirectly for deep linking and attribution, and supplement with privacy-preserving fraud tools if needed. ## Explore more alternatives - [Firebase Dynamic Links Alternative](https://redirectly.app/firebase-dynamic-links-alternative.md) - [Branch Alternative](https://redirectly.app/branch-alternative.md) - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [AASA Validator Tool](https://redirectly.app/aasa-validator.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly vs Dub > Compare Redirectly vs Dub. Dub shortens links for the web; Redirectly is full-stack mobile deep linking with app install attribution. See which fits you. - Canonical: https://redirectly.app/dub-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Understanding the difference between a web link shortener and a mobile deep linking platform. Dub shortens links for web; Redirectly enables deep linking with app install attribution. ## The fundamental difference ### Dub **Link shortener** for web and social media - Shortens long URLs into branded short links - Analytics: clicks, referrers, geolocation - Best for: marketing campaigns, Twitter bios, emails ### Redirectly **Mobile deep linking platform** with install attribution - Routes users directly to specific app screens - Preserves context through app install (deferred) - Best for: app onboarding, referrals, email verification ### The key distinction **Dub:** Shortens a link like `"https://example.com/very/long/path"` to `"https://dub.co/abc123"`. When clicked on desktop, the user goes to the web page. When clicked on mobile, if your app is installed, you can open the app instead—but Dub doesn't handle this natively. **Redirectly:** Creates deep links that open your app **or web** with context preserved. If the app isn't installed, Redirectly stores the intended content and delivers it when the app is installed for the first time. Dub doesn't do this. ## Feature-by-feature comparison | Feature | Redirectly | Dub | | --- | --- | --- | | Link shortening | Yes | Yes | | Mobile app deep linking | Yes | Limited | | Deferred deep linking (through app install) | Yes | No | | Install attribution | Yes | No | | Mobile SDK (Flutter/React Native) | Yes | No | | Custom subdomains | Yes | Yes | | AASA/assetlinks hosting | Yes | No | | Web redirects | Yes | Yes | | Click analytics | Yes | Yes | | A/B testing | No | Yes | | QR code generation | No | Yes | | Webhook support | Yes | Yes | ## When to use each platform ### Use Dub when you need: - **Web link shortening** for blogs, emails, Twitter, LinkedIn - **Click-through analytics** on web—who clicked, where they came from - **A/B testing URLs**—test different landing pages - **QR codes** for offline campaigns - **Simple branded links**—marketing campaigns ### Use Redirectly when you need: - **Mobile app deep linking**—open specific screens in your app - **Deferred deep linking**—preserve context through app install - **Install attribution**—know which referrals led to installs - **Referral programs**—track which users referred new installs - **Email verification flows**—preserve verification tokens through install - **Personalized onboarding**—guide users to relevant content ### Use both together: Dub and Redirectly solve different problems and can complement each other: - **Dub** for web marketing—shorten links in blog posts, social media - **Redirectly** for app campaigns—deep link to specific app screens ## How they work differently in code ### Dub: Shortening a web link ```javascript // Dub API - Create a short link const shortLink = await fetch('https://api.dub.co/links', { method: 'POST', headers: { 'Authorization': 'Bearer dub_key' }, body: JSON.stringify({ url: 'https://example.com/very/long/marketing/campaign/page', domain: 'mycompany.link', }), }); // Result: https://mycompany.link/abc123 // When clicked on desktop: goes to web page // When clicked on mobile: goes to web page (no app deep linking) ``` Dub creates short URLs for web. When users click on mobile, they still land on the web page. No app integration. ### Redirectly: Deep link to app screen ```javascript // Redirectly API - Create a deep link const deepLink = await fetch('https://api.redirectly.app/v1/links', { method: 'POST', headers: { 'Authorization': 'Bearer api_key' }, body: JSON.stringify({ path: 'product/blue-shoes', queryParams: { size: '10', color: 'blue' }, }), }); // Result: https://myapp.redirectly.app/product/blue-shoes?size=10&color=blue // When clicked on mobile with app installed: app opens to product screen // When clicked on mobile without app: stores intent, prompts install, // on first launch app opens to product screen (deferred) ``` Redirectly creates deep links for apps. Preserves context even if the app needs to be installed first. ### In your Flutter app with Redirectly ```dart import 'package:redirectly/redirectly.dart'; // On app launch, get the deep link (works even if just installed) final deepLink = await Redirectly.getInitialLink(); if (deepLink != null) { if (deepLink.path == 'product') { final productId = deepLink.queryParameters['product']; // Navigate to product screen context.go('/product/$productId'); } } // With Dub short links, you'd manually parse the redirect target // With Redirectly, the SDK handles this automatically ``` ## Related - [Redirectly vs Branch](https://redirectly.app/branch-alternative.md) - [Redirectly vs Firebase](https://redirectly.app/firebase-dynamic-links-alternative.md) - [Redirectly vs Kochava](https://redirectly.app/kochava-alternative.md) - [What is Deferred Deep Linking?](https://redirectly.app/deferred-deep-linking.md) ## Frequently asked questions ### Can I use Dub for mobile app deep linking? Dub can shorten links, but it doesn't have native support for deferred deep linking or install attribution. If you need the app to open to a specific screen, you'd need to build custom logic. Redirectly handles this out of the box. ### Does Redirectly shorten links like Dub? Redirectly creates deep links that are functional URLs. They're not as short as Dub links (which use short domains), but they're readable and packed with context. For purely cosmetic link shortening, Dub is better. For app deep linking, Redirectly is necessary. ### Should I use both Dub and Redirectly? You could. Use Dub for web marketing links (blog posts, Twitter), and Redirectly for app campaigns. But many teams find Redirectly's deep links sufficient for both web and app contexts. ### Does Dub have deferred deep linking? Not natively. Dub is primarily a web link shortener. It can redirect to an app store, but it won't preserve context when the app installs for the first time. Redirectly is designed specifically for this use case. ### Is Redirectly more expensive than Dub? Dub has free and paid tiers focused on link shortening. Redirectly has free and paid tiers focused on app deep linking. Pricing depends on your use case. For app deep linking, Redirectly is a necessity; Dub can't replace it. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly vs Kochava > Compare Redirectly vs Kochava: deferred deep linking, developer experience, pricing, and Flutter support. Simpler attribution, deeper deep linking. - Canonical: https://redirectly.app/kochava-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Comprehensive comparison of Redirectly and Kochava: features, pricing, developer experience, and when to use each platform. Redirectly focuses on deferred deep linking; Kochava focuses on enterprise attribution. ## Understanding the platforms ### Redirectly **Focus:** Deferred deep linking and install attribution for mobile developers. - Pure deep linking solution - Developer-first pricing and UX - Transparent, self-serve pricing - Flutter, React Native, iOS, Android ### Kochava **Focus:** Enterprise-grade mobile attribution and analytics platform. - Full attribution stack - Multi-channel attribution - Enterprise sales and support - Advanced cohort analysis **The key difference:** Redirectly specializes in deferred deep linking—ensuring links work seamlessly even when the app isn't installed. Kochava specializes in enterprise attribution—tracking installs across many channels. These are complementary but focused on different problems. ## Feature comparison | Feature | Redirectly | Kochava | | --- | --- | --- | | Deferred deep linking | Yes | Yes | | Install attribution | Yes | Yes | | Deep link retargeting | Yes | No | | Multi-touch attribution | No | Yes | | Cross-channel attribution | No | Yes | | Custom subdomains | Yes | No | | AASA/assetlinks hosting | Yes | No | | Flutter SDK | Yes | No | | React Native SDK | Yes | Yes | | Real-time analytics | Yes | Yes | | Cohort analysis | No | Yes | | Self-serve pricing | Yes | No | | Free tier | Yes | No | ## Pricing comparison ### Redirectly #### Free $0 - 10k monthly links - Basic analytics - Community support #### Starter $24/mo - 100k monthly links - Email support #### Pro $80/mo - Unlimited links - Priority support ### Kochava #### Starter Available for enterprise customers Contact sales #### Enterprise Most customers start here $10,000+ /month **Notes:** Kochava focuses on enterprise customers. No free tier. Multi-touch attribution and cohort analysis included at all tiers. ### For deferred deep linking: $24-80/mo (Redirectly) vs $10,000+/mo (Kochava) ## When to use Redirectly vs Kochava ### Use Redirectly when you need: - **Deferred deep linking** that actually works—preserve context through app installs - **Developer-friendly setup**—pure Dart/Flutter SDKs, no native code overhead - **Transparent pricing**—know exactly what you're paying upfront - **Email verification, referral programs, or personalized onboarding**—deep linking use cases - **Budget-conscious teams**—$24-80/month for production use ### Use Kochava when you need: - **Multi-touch attribution**—track installs across multiple ad channels simultaneously - **Advanced cohort analysis**—segment users by install source for LTV optimization - **Enterprise support**—dedicated account managers and custom integrations - **Cross-channel campaign tracking**—optimize spend across Facebook, Google, TikTok, etc. - **Large-scale apps**—you're tracking millions of installs ### Use them together: Redirectly and Kochava serve different purposes. You can use **both simultaneously**: - **Redirectly** for deep linking (email verification, referrals, onboarding) - **Kochava** for multi-touch attribution (understanding which ad channels drive installs) ## Related - [Redirectly vs Branch](https://redirectly.app/branch-alternative.md) - [Redirectly vs Firebase](https://redirectly.app/firebase-dynamic-links-alternative.md) - [Redirectly vs Dub](https://redirectly.app/dub-alternative.md) - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) ## Frequently asked questions ### Does Kochava have deferred deep linking? Kochava supports basic deep linking, but deferred deep linking (preserving context through install) is not a core feature. Redirectly specializes in this. ### Can I migrate from Kochava to Redirectly? Yes. The platforms serve different purposes. You can stop using Kochava's deep linking and use Redirectly's instead. Attribution data won't transfer, but going forward you'll have better deep linking. ### Does Redirectly do multi-touch attribution? Not at this time. Redirectly focuses on deferred deep linking and single-touch install attribution. For complex multi-touch attribution, you'd still want a platform like Kochava. ### Can small teams afford Kochava? Kochava's minimum is typically $10k+/month, making it cost-prohibitive for small teams. Redirectly starts free and scales to $80/month, making it much more accessible. ### Which is better for Flutter apps? Redirectly offers a pure Dart Flutter SDK with no native code required. Kochava's Flutter support exists but is more enterprise-focused. Redirectly is the simpler choice for Flutter. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Airbridge Alternative > Replace Airbridge with Redirectly: better developer experience, clear pricing, pure Dart SDKs, and stronger deep linking. Compare features and migration. - Canonical: https://redirectly.app/airbridge-alternative - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Why developers choose Redirectly over Airbridge: better developer experience, transparent pricing, pure Dart SDKs, and stronger deep linking support. Complete feature comparison and migration guide. ## Why developers seek Airbridge alternatives ### Redirectly **Focus:** Deferred deep linking and install attribution built for developers. - Pure Dart SDK (no native code) - Transparent pricing starting $24/mo - Strong React Native support - Developer-first architecture ### Airbridge **Focus:** Enterprise attribution and deep linking platform. - Comprehensive attribution stack - Multi-touch attribution - Enterprise support model - Complex SDK integration **The key difference:** Redirectly is a pure deep linking solution with transparent pricing and developer-friendly APIs. Airbridge is an enterprise attribution platform with complex SDKs and enterprise-only pricing. For developers prioritizing simplicity and cost-effectiveness, Redirectly is the better choice. ## Feature comparison | Feature | Redirectly | Airbridge | | --- | --- | --- | | Deep linking | Yes | Yes | | Deferred deep linking | Yes | Yes | | Install attribution | Yes | Yes | | Flutter SDK (pure Dart) | Yes | No | | React Native SDK | Yes | Yes | | Custom subdomains | Yes | No | | AASA/assetlinks hosting | Yes | No | | MCP Server | Yes | No | | Privacy-first analytics | Yes | No | | Real-time analytics | Yes | Yes | | Free tier | Yes | No | ## Pricing comparison ### Redirectly #### Free $0 - 10k monthly links - Basic analytics - Community support #### Starter $24/mo - 100k monthly links - Email support #### Pro $80/mo - Unlimited links - Priority support ### Airbridge #### Starter Limited availability Contact sales #### Enterprise Minimum commitment $5,000+ /month **Notes:** Airbridge focuses on enterprise customers with volume-based pricing. No transparent pricing model. Requires sales consultation. ### Cost comparison: $24-80/mo (Redirectly) vs $5,000+/mo (Airbridge) ## Developer experience ### Redirectly approach: - **Built by developers, for developers**—simple APIs, minimal configuration - **Pure Dart SDK**—no native code overhead for Flutter apps - **REST API first**—integrate anywhere without SDK bloat - **Clear documentation**—migration guides and code examples ### Airbridge approach: - **Marketing-first platform**—features designed for campaign teams - **Complex SDK initialization**—native code required for full features - **Enterprise support model**—sales-driven rather than self-serve - **Long onboarding**—implementation services required ## Migration from Airbridge ### Step 1: Initialize Redirectly alongside Airbridge Start with both platforms running in parallel. This allows you to validate Redirectly's behavior without losing Airbridge functionality. ```dart // Flutter example import 'package:redirectly/redirectly.dart'; void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); // Initialize Redirectly Redirectly.init(apiKey: 'your_api_key'); // Keep Airbridge initialization // AirBridge.init(...); } @override Widget build(BuildContext context) { return MaterialApp(home: HomePage()); } } ``` ### Step 2: Handle deep links with Redirectly Replace Airbridge deep link handling with Redirectly's simpler API. ```dart // Before (Airbridge) AirBridge.handleDeeplink((deeplink) { if (deeplink != null && deeplink.values != null) { String page = deeplink.values?['page'] ?? ''; navigateToPage(page); } }); // After (Redirectly) Redirectly.handleDeeplink((deeplink) { String page = deeplink.getParameter('page') ?? ''; navigateToPage(page); }); ``` ### Step 3: Validate and sunset Airbridge Monitor deep link performance, compare analytics, then remove Airbridge SDK when confident. ```dart // Check deep link resolution Redirectly.getDeferredDeeplink().then((deeplink) { if (deeplink != null) { print('Resolved: ${deeplink.url}'); // Log to your analytics analytics.logEvent('deeplink_resolved', deeplink); } }); ``` ### Migration timeline: - **Day 1-2:** Add Redirectly SDK, update iOS/Android config files (AASA/assetlinks) - **Day 3-4:** Update deep link handlers, test all link types - **Day 5-7:** Monitor analytics, validate deferred deep linking works - **Week 2:** Remove Airbridge SDK, reduce build size ## Related - [Redirectly vs Branch](https://redirectly.app/branch-alternative.md) - [Redirectly vs Firebase](https://redirectly.app/firebase-dynamic-links-alternative.md) - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) ## Frequently asked questions ### Is Airbridge stopping support? While Airbridge continues development, many teams migrate to Redirectly for better developer experience, transparent pricing, and more focused deep linking capabilities. ### How do I migrate from Airbridge to Redirectly? Migration is straightforward: initialize Redirectly SDK alongside Airbridge, update deep link handling logic, validate analytics, then remove Airbridge SDK. Typical migration takes 1-2 weeks. ### Does Redirectly have feature parity with Airbridge? Redirectly covers core deep linking and install attribution features that most apps need. Redirectly excels in developer experience, pricing transparency, and pure Dart SDK support. For complex multi-touch attribution, Airbridge may still be needed. ### What about our existing Airbridge SDK integration? Redirectly provides pure Dart SDKs (no native code overhead) and REST APIs. Most integrations migrate in 1-2 days. You can run both in parallel during transition. ### Can I use Redirectly and Airbridge together? Yes, run both in parallel during migration. This lets you validate Redirectly works before fully sunsetting Airbridge. ### How much will we save switching to Redirectly? Redirectly starts at $24/month vs Airbridge enterprise pricing ($5,000+/month). Most teams save 95%+ on direct platform costs. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # App Onboarding with Deep Links > Create personalized onboarding with deferred deep links. Guide new users straight to relevant content on first app launch and lift activation and retention. - Canonical: https://redirectly.app/use-cases/app-onboarding - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Create personalized first-run experiences with deferred deep links. Guide new users directly to the most relevant content for them on their first app launch. ## The personalization opportunity First impressions matter. When users install your app, you have a critical moment to show them what's most relevant to them. Without deferred deep links, that moment is lost. ### Why personalized onboarding matters - **First-time user activation:** Users are most engaged in the first 5 minutes. Show them relevant content immediately. - **Context from the web:** User came from a product page, category, or specific content. Preserve that context in the app. - **Skip irrelevant steps:** Don't force everyone through generic onboarding. Route users based on their entry point. - **Higher conversion:** Users who see relevant content convert at higher rates than those shown a generic home screen. ### Generic onboarding 1. User clicks link to see specific product 2. App installs (if needed) 3. Generic welcome screen appears 4. User must search for the product they wanted 5. Friction. User may abandon. ### Personalized with deep links 1. User clicks link to see specific product 2. App installs (if needed) 3. App opens directly to that product 4. No friction. User sees exactly what they wanted. 5. Higher likelihood of purchase or engagement. ## How deferred deep links enable personalization ### Step 1: User on web or email A user views a specific product on your website or in a marketing email. You create a deep link to that product: `https://myapp.redirectly.app/product/blue-shoes-size-10` ### Step 2: User taps link on mobile User taps the link on their phone. If the app is installed, it opens directly to the product screen. If the app is not installed, Redirectly captures the full URL including product ID and stores it. ### Step 3: Install and redirect User is sent to App Store or Play Store. The product context is preserved in Redirectly's servers. ### Step 4: App retrieves deferred link on launch When the app launches for the first time, the Redirectly SDK queries: "What page should this user see?" Redirectly responds: "/product/blue-shoes-size-10". ### Step 5: App navigates to product Your app receives the deep link and navigates directly to the product page. User sees exactly what they wanted. First impression is perfect. ## Onboarding scenarios powered by deferred deep links ### E-commerce: Product page User sees a shoe on social media. Clicks link. Doesn't have your shopping app. App installs and opens directly to that shoe product, size and color included. **Result:** One-tap purchase. No searching required. ### Streaming: Watch specific video Friend shares a YouTube video link via your streaming app's sharing feature. Recipient doesn't have the app yet. App installs and opens directly to that video. **Result:** Instant video playback. No discovery friction. ### Social: User profile Someone shares their profile link. New user clicks it. App installs and opens directly to that person's profile, already ready to follow or message. **Result:** Immediate connection. Lower friction to social engagement. ### Marketplace: Category or search results Marketing campaign sends users to a specific category (e.g., "Winter Jackets"). Users without the app get it installed and see that category first. **Result:** Campaign relevance maintained. Higher campaign ROI. ## Implementation guide ### Step 1: Generate contextual deep links Whenever a user could install your app, create a deep link with context: ```python # Web backend - Generate deep link with context def generate_deep_link(product_id, user_id=None, category=None): params = {'product': product_id} if category: params['category'] = category if user_id: params['user'] = user_id deep_link = build_redirectly_url(params) return deep_link # Usage: On product pages, in emails, in social shares product_link = generate_deep_link( product_id='blue_shoes_10', category='footwear' ) # Result: https://myapp.redirectly.app/shop?product=blue_shoes_10&category=footwear ``` ### Step 2: Initialize and listen for deferred link In your Flutter app, initialize Redirectly and listen for the deferred link on first launch: ```dart import 'package:redirectly/redirectly.dart'; import 'package:go_router/go_router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Get deferred link on first launch final initialLink = await Redirectly.getInitialLink(); // Determine initial route based on deep link String initialRoute = '/home'; if (initialLink != null) { initialRoute = _routeFromDeepLink(initialLink); } runApp(MyApp(initialRoute: initialRoute)); } String _routeFromDeepLink(RedirectlyLink link) { switch (link.path) { case 'product': final productId = link.queryParameters['product']; return '/product/$productId'; case 'category': final categoryId = link.queryParameters['category']; return '/category/$categoryId'; case 'profile': final userId = link.queryParameters['user']; return '/profile/$userId'; default: return '/home'; } } ``` ### Step 3: Route to correct screen on startup Use go_router to navigate to the personalized screen on app launch: ```dart final GoRouter _buildRouter(String initialRoute) { return GoRouter( initialLocation: initialRoute, routes: [ GoRoute( path: '/home', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; return ProductDetailScreen(productId: productId); }, ), GoRoute( path: '/category/:categoryId', builder: (context, state) { final categoryId = state.pathParameters['categoryId']!; return CategoryScreen(categoryId: categoryId); }, ), GoRoute( path: '/profile/:userId', builder: (context, state) { final userId = state.pathParameters['userId']!; return UserProfileScreen(userId: userId); }, ), ], ); } ``` ## Complete onboarding example ```dart import 'package:flutter/material.dart'; import 'package:redirectly/redirectly.dart'; import 'package:go_router/go_router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Get deferred deep link final initialLink = await Redirectly.getInitialLink(); // Determine initial route String initialRoute = '/home'; if (initialLink != null) { initialRoute = _getRouteFromDeepLink(initialLink); } runApp(MyApp(initialRoute: initialRoute)); } String _getRouteFromDeepLink(RedirectlyLink link) { if (link.path == 'product') { final productId = link.queryParameters['product']; return '/product/$productId'; } else if (link.path == 'category') { final categoryId = link.queryParameters['category']; return '/category/$categoryId'; } return '/home'; } class MyApp extends StatelessWidget { final String initialRoute; const MyApp({required this.initialRoute}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: GoRouter( initialLocation: initialRoute, routes: [ GoRoute( path: '/home', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: '/category/:categoryId', builder: (context, state) { final categoryId = state.pathParameters['categoryId']!; return CategoryScreen(categoryId: categoryId); }, ), ], ), ); } } class ProductScreen extends StatelessWidget { final String productId; const ProductScreen({required this.productId}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Product')), body: FutureBuilder( future: fetchProduct(productId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.loading) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}')); } if (!snapshot.hasData) { return const Center(child: Text('Product not found')); } final product = snapshot.data!; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.network(product.imageUrl), Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( '$${product.price}', style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Colors.green, ), ), const SizedBox(height: 16), ElevatedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Added to cart!')), ); }, child: const Text('Add to Cart'), ), ], ), ), ], ), ); }, ), ); } Future fetchProduct(String id) async { // Fetch product details from your API // This is called immediately on app launch return Product( id: id, name: 'Sample Product', price: 29.99, imageUrl: 'https://example.com/product.jpg', ); } } class Product { final String id; final String name; final double price; final String imageUrl; Product({ required this.id, required this.name, required this.price, required this.imageUrl, }); } ``` This example shows: - **Deferred link retrieval** on app launch - **Dynamic routing** based on deep link content - **go_router integration** for seamless navigation - **Data loading** while showing the right screen ## Next steps Learn more about onboarding and deferred deep linking: - [Read: App Onboarding with Deferred Deep Linking](https://redirectly.app/blog/app-onboarding-deferred-deep-linking.md) - [Learn: What is Deferred Deep Linking?](https://redirectly.app/deferred-deep-linking.md) - [Guide: Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) ## Frequently asked questions ### What if the user has the app already installed? If the app is installed, the deep link opens immediately without needing deferred link lookups. It's instant. Deferred links are a safety net for when the app isn't installed yet. ### Can I personalize the onboarding screen itself? Yes. You can pass context parameters in the deep link to show different onboarding flows. For example, new e-commerce users could see product tips, while social users see connection suggestions. ### How do I handle invalid product IDs in the deep link? Check if the product exists before navigating. If invalid, navigate to home instead. The user can always find the product through your app's normal search/browse. ### Does this work with analytics? Yes. You can log which deep links users arrived from, track how often each page is a first impression, and measure conversion from personalized onboarding. ### Can I A/B test onboarding flows? Absolutely. Pass an experiment variant in the deep link, and show different UX based on it. Measure which onboarding flow converts better. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Email Verification Deep Links > Solve email verification for mobile apps using deferred deep links. Redirectly preserves verification context when users tap links before installing your app. - Canonical: https://redirectly.app/use-cases/email-verification - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Verify users seamlessly with deferred deep links. Preserve verification context even when users tap verification emails before installing your app. ## The email verification problem Email verification links break a critical user flow for uninstalled apps. Here's the typical scenario: ### What happens without deep links: 1. User signs up via web or email. They don't have your app installed yet. 2. Verification email arrives. User taps the link on mobile. 3. Browser opens the verification URL. App isn't installed, so verification page is incomplete. 4. User is prompted to install app. Even after install, they lose context—don't know what to verify. 5. User abandons flow. Sign-up conversion drops. ### Without deferred deep links - Verification token lost when navigating to app - User sees generic app home screen, not verification context - Manual re-verification needed (friction) - Higher app abandonment in onboarding ### With deferred deep links - Verification token preserved across app install - App opens directly to verification completion screen - One-tap confirmation in app after install - Seamless onboarding experience ## How deferred deep links solve email verification Deferred deep links preserve your verification token and context even when the app isn't installed at the time the user taps the link. ### Step 1: User receives email Your backend sends a verification email with a Redirectly deep link containing the verification token: `https://myapp.redirectly.app/verify?token=abc123&email=user@example.com` ### Step 2: User taps link on mobile If the app is **already installed**, the deep link opens directly to your verification screen with token intact. If the app is **not installed**, Redirectly captures the full URL (token included) and stores it. ### Step 3: App store redirect User is sent to App Store or Play Store to install your app. The verification token is **preserved** in Redirectly's servers. ### Step 4: SDK retrieves deferred link When your app launches for the first time, the Redirectly SDK queries the servers: "What deep link was intended for this device?" Redirectly responds with the full verification URL including the token. ### Step 5: App navigates to verification screen Your app receives the deep link with the token and navigates to the verification completion screen, ready for one-tap confirmation. Token is intact. User is delighted. ## Implementation with Redirectly ### Backend: Generate verification links When a user signs up, create a Redirectly deep link with the verification token as a query parameter: ```python # Example backend code (Python) verification_token = generate_token() deep_link = ( f"https://myapp.redirectly.app/verify" f"?token={verification_token}" f"&email={user_email}" ) # Send email with deep_link send_verification_email(user_email, deep_link) ``` ### Mobile: Setup and listen for deep links Initialize Redirectly in your Flutter app and listen for deep link events: ```dart import 'package:redirectly/redirectly.dart'; import 'package:go_router/go_router.dart'; void main() async { // Initialize Redirectly with your API key await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Listen for deferred deep links Redirectly.onDeepLink.listen((link) { print('Received deferred link: ${link.fullUrl}'); }); runApp(const MyApp()); } ``` ### Navigation: Handle verification deep links When the app receives a verification deep link, navigate to the verification screen: ```dart // In your main.dart or routing setup Redirectly.onDeepLink.listen((link) { // Parse the deep link if (link.path == 'verify') { final token = link.queryParameters['token']; final email = link.queryParameters['email']; // Navigate to verification screen with token context.go('/verify?token=$token&email=$email'); } }); // Or with go_router route guards final goRouter = GoRouter( routes: [ GoRoute( path: '/verify', builder: (context, state) { final token = state.queryParameters['token']; final email = state.queryParameters['email']; return EmailVerificationScreen( token: token, email: email, ); }, ), ], ); ``` ## Complete Dart/Flutter example ```dart import 'package:flutter/material.dart'; import 'package:redirectly/redirectly.dart'; import 'package:go_router/go_router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Redirectly.initialize( apiKey: 'your_api_key_here', ); runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); // Listen for deep links (both foreground and deferred) Redirectly.onDeepLink.listen((link) { _handleDeepLink(link); }); } void _handleDeepLink(RedirectlyLink link) { if (link.path == 'verify') { final token = link.queryParameters['token']; final email = link.queryParameters['email']; // Navigate to verification screen context.go('/verify?token=$token&email=$email'); } } @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _buildRouter(context), ); } GoRouter _buildRouter(BuildContext context) { return GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/verify', builder: (context, state) { final token = state.queryParameters['token']; final email = state.queryParameters['email']; return EmailVerificationScreen( token: token ?? '', email: email ?? '', ); }, ), ], ); } } class EmailVerificationScreen extends StatefulWidget { final String token; final String email; const EmailVerificationScreen({ required this.token, required this.email, Key? key, }) : super(key: key); @override State createState() => _EmailVerificationScreenState(); } class _EmailVerificationScreenState extends State { bool _isVerifying = false; Future _verifyEmail() async { setState(() => _isVerifying = true); try { // Send token to your backend to verify final response = await http.post( Uri.parse('https://api.example.com/verify-email'), body: { 'token': widget.token, 'email': widget.email, }, ); if (response.statusCode == 200) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Email verified! Welcome!')), ); context.go('/home'); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $e')), ); } } finally { setState(() => _isVerifying = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Verify Email')), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.mail_outline, size: 64), const SizedBox(height: 16), Text( 'Verify your email', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( 'Confirm your email: ${widget.email}', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 32), ElevatedButton( onPressed: _isVerifying ? null : _verifyEmail, child: _isVerifying ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Text('Verify Email'), ), ], ), ), ), ); } } ``` This example shows the complete flow: - **Initialize Redirectly** in main.dart with your API key - **Listen for deep links** from Redirectly (works even if app was just installed) - **Parse the token** from the deep link query parameters - **Navigate to verification screen** with token intact - **Verify with your backend** using the token ## Next steps Learn more about email verification and deferred deep linking: - [Read: Email Verification Deep Linking Guide](https://redirectly.app/blog/email-verification-deep-linking.md) - [Learn: What is Deferred Deep Linking?](https://redirectly.app/deferred-deep-linking.md) - [Guide: Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) ## Frequently asked questions ### Does this work if the app is already installed? Yes. If your app is installed when the user taps the verification link, the deep link opens immediately with the token intact. If the app isn't installed, Redirectly stores the token and delivers it after install. ### What if the user never installs the app? The verification link still works in the browser. The user can verify via web, then install the app later. Deferred deep links are an enhancement for better UX, not a replacement for web verification. ### Can I use this with React Native? Yes. Redirectly works with Flutter and React Native. The implementation is similar—initialize the SDK, listen for deep links, and parse the token from the URL. ### How long are tokens stored? By default, Redirectly stores deferred deep link data for 30 days. You can configure this in your dashboard. After that period, the link expires. ### Is this secure? Can someone intercept the token? Deep links travel in URLs, which can be logged. For maximum security, use short-lived tokens that expire quickly (5-15 minutes), and require user verification of their email address before activation. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Paid Ads Deep Linking & Attribution > Maximize ad ROI with deep linking. Track conversions through install, preserve campaign context, and measure ROAS for Facebook and Google Ads campaigns. - Canonical: https://redirectly.app/use-cases/paid-ads - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Measure the true ROI of your ad spend. Use deferred deep links to track conversions through install, preserve campaign context, and optimize ad performance with attribution data. ## The paid ads attribution problem Mobile app install ads are a major spend category, but measuring their ROI is broken. Without deep linking, you can't track what happens after someone installs your app from an ad. ### What happens without deep links: 1. User clicks your ad on Facebook or Google. They're sent to app store to install. 2. App installs. User launches it for the first time. 3. They see your home screen. What were they supposed to do? The ad message is lost. 4. If they don't convert immediately, you can't track attribution. Did this ad drive the purchase, or was it another touchpoint? 5. You can't measure ROAS accurately. Ad spend increases, but ROI remains unclear. ### Without deep links - Ad context lost when app installs - No attribution for post-install conversions - Can't measure true ROAS - Users see generic home screen, not ad-targeted content ### With deferred deep links - Ad campaign context preserved through install - Track conversions directly attributed to ads - Measure accurate ROAS for each campaign - App opens to ad-specific content, boosting conversion ## How deferred deep links solve paid ad attribution Deferred deep links preserve campaign data and user context through the app install process, enabling accurate ROI measurement. ### Step 1: Create campaign-specific deep links Create unique Redirectly deep links for each ad campaign with UTM parameters and targeting data: `https://myapp.redirectly.app/offer?utm_source=facebook&utm_campaign=summer_sale&offer_id=summer2024` ### Step 2: User clicks ad User sees your ad on Facebook or Google Ads. They click and are sent to your app store listing. Redirectly captures the full deep link URL with all campaign parameters. ### Step 3: App install User installs your app from the store. The campaign data (UTM parameters, offer ID) is **preserved** in Redirectly's servers, associated with this user's device. ### Step 4: SDK retrieves campaign context When the app launches for the first time, the Redirectly SDK queries: "What campaign should this user see?" Redirectly responds with the full deep link, including campaign parameters and offer ID. ### Step 5: Log attribution event Your app logs an attribution event to your analytics and ad platform. The event includes the campaign source, campaign name, and other UTM parameters. Conversions are now attributed to the correct ad campaign. ### Step 6: Measure ROAS Track conversions (purchases, signups, etc.) attributed to each ad campaign. Calculate true ROAS by dividing revenue from ad-driven users by total ad spend. Optimize budgets based on data. ## Facebook and Google Ads deep linking ### Facebook Ads Integration Facebook App Installs campaigns can include deep links in the "Deep Link" field or through app events. Use Redirectly to create campaign-specific deep links that preserve UTM parameters through install. Log conversion events with the campaign attribution data so Facebook can optimize future campaigns. **Key parameters:** utm_source=facebook, utm_medium=cpc, utm_campaign=[campaign_name] ### Google Ads Integration Google App Campaigns automatically add GCLID (Google Click ID) to deep links for attribution. Combine GCLID with Redirectly's deferred deep linking to preserve campaign context through install. Send conversion events back to Google with GCLID for attribution. **Key parameters:** utm_source=google, utm_medium=cpc, gclid=[auto], utm_campaign=[campaign_name] ### UTM Parameter Preservation Redirectly preserves all UTM parameters (source, medium, campaign, content, term) through the install process. Your app receives the exact same UTM parameters in the deferred deep link as were in the original ad click. This enables accurate multi-touch attribution and campaign analysis. ## Attribution tracking implementation ### Setup and initialization Initialize Redirectly and your analytics platform: ```dart import 'package:redirectly/redirectly.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Initialize Firebase Analytics FirebaseAnalytics analytics = FirebaseAnalytics.instance; // Listen for deferred deep links from ads Redirectly.onDeepLink.listen((link) { _handleAdDeepLink(link, analytics); }); runApp(const MyApp()); } ``` ### Handle attribution deep links Parse campaign parameters and log attribution events: ```dart Future _handleAdDeepLink( RedirectlyLink link, FirebaseAnalytics analytics, ) async { // Extract UTM parameters final utmSource = link.queryParameters['utm_source']; final utmMedium = link.queryParameters['utm_medium']; final utmCampaign = link.queryParameters['utm_campaign']; final utmContent = link.queryParameters['utm_content']; final gclid = link.queryParameters['gclid']; // Log attribution event to analytics await analytics.logEvent( name: 'ad_attribution', parameters: { 'utm_source': utmSource, 'utm_medium': utmMedium, 'utm_campaign': utmCampaign, 'utm_content': utmContent, if (gclid != null) 'gclid': gclid, }, ); // Store attribution data for later use final prefs = await SharedPreferences.getInstance(); await prefs.setString('utm_campaign', utmCampaign ?? ''); await prefs.setString('utm_source', utmSource ?? ''); // Navigate to campaign-specific content if (link.path == 'offer') { final offerId = link.queryParameters['offer_id']; // Navigate to offer screen context.go('/offer?id=$offerId'); } } ``` ### Log conversion events When a user converts, include attribution data in the event: ```dart Future _logPurchase( double revenue, String itemId, FirebaseAnalytics analytics, ) async { final prefs = await SharedPreferences.getInstance(); final utmCampaign = prefs.getString('utm_campaign') ?? 'direct'; final utmSource = prefs.getString('utm_source') ?? 'direct'; // Log purchase with attribution data await analytics.logPurchase( currency: 'USD', value: revenue, items: [ AnalyticsEventItem( itemId: itemId, itemName: 'Product Purchase', price: revenue, ), ], parameters: { 'utm_campaign': utmCampaign, 'utm_source': utmSource, }, ); // Send event to ad platform for conversion tracking _reportConversionToAdPlatform( revenue: revenue, campaign: utmCampaign, ); } ``` ### Report conversions back to ad platforms Send conversion data back to Facebook and Google for ROAS calculation: ```dart Future _reportConversionToAdPlatform({ required double revenue, required String campaign, }) async { const String facebookConversionUrl = 'https://graph.facebook.com/v18.0/$PIXEL_ID/events'; // Report to Facebook final facebookPayload = { 'data': [ { 'event_name': 'Purchase', 'event_time': DateTime.now().millisecondsSinceEpoch ~/ 1000, 'currency': 'USD', 'value': revenue.toString(), 'event_source_url': 'https://myapp.example.com', } ], 'access_token': 'YOUR_PIXEL_TOKEN', }; try { await http.post( Uri.parse(facebookConversionUrl), body: jsonEncode(facebookPayload), headers: {'Content-Type': 'application/json'}, ); print('Conversion reported to Facebook'); } catch (e) { print('Error reporting to Facebook: $e'); } // Google Ads will receive conversion via Firebase if properly configured } ``` This implementation ensures: - **Campaign data preserved** through app install via deferred deep links - **UTM parameters captured** and stored for later use in conversion tracking - **Conversions attributed** to the correct ad campaign with full context - **ROAS measured accurately** by reporting conversions back to ad platforms ## Next steps Learn more about deferred deep linking and ad attribution: - [Learn: What is Deferred Deep Linking?](https://redirectly.app/deferred-deep-linking.md) - [Guide: Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [Guide: React Native Deferred Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) ## Frequently asked questions ### How does deferred deep linking improve ROAS? By preserving campaign context through install, users land on relevant content instead of a generic home screen. This increases engagement and conversion rates. Additionally, you can accurately attribute conversions to the campaigns that drove them, allowing you to optimize budgets toward high-performing campaigns. ### Does this work with Facebook and Google Ads simultaneously? Yes. Use consistent UTM parameters across both platforms. Redirectly preserves all UTM data through install, and you can report conversions to both platforms. This gives you attribution data in both Facebook Ads Manager and Google Ads. ### What if the user already has the app installed when they click the ad? The deep link opens immediately in the app (not the app store). The user is taken directly to the campaign-specific content. Redirectly handles this seamlessly—no special configuration needed. ### Can I track multi-touch attribution with deferred deep links? Yes. Store attribution data from the first app launch, then append it to all subsequent conversion events. This shows the original campaign that drove the install, even if other touchpoints occur later. ### Do I need to modify my ad campaigns to use this? Minimally. Create Redirectly deep links with UTM parameters, then add those deep links to your ad campaigns. Facebook and Google Ads both support deep link fields. That's it—your existing ad setup works with deferred deep linking. ### What happens if my server is down when the user installs? Redirectly's servers are highly available (99.99% uptime). But if a timeout occurs, your app can still function—just without the deferred deep link data. Users would see your home screen instead of campaign-specific content. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # QR Code Deep Linking > Generate QR codes that link straight into your mobile app. Deferred deep linking handles uninstalled apps, tracks campaigns, and drives app installs. - Canonical: https://redirectly.app/use-cases/qr-codes - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Create QR codes that link directly to your app. Use deferred deep linking to handle uninstalled apps, preserve context, and track campaigns from restaurants, retail, and events. ## The QR code to app problem QR codes are everywhere: restaurant menus, event tickets, retail displays, and marketing materials. But when users scan a QR code on mobile and don't have your app installed, the experience breaks down. **What happens without deep links:** 1. User at restaurant/event scans QR code on their phone. 2. Browser opens, showing a landing page instead of app content. 3. User is prompted to install app from app store. Context is lost. 4. After install, user returns to app home screen. What were they looking for? They don't remember. 5. User abandons. You lose a customer interaction and can't track the campaign. **Without deferred deep links:** - QR code context lost when app installs - Users see generic home screen, not intended content - No campaign attribution or tracking - High drop-off rate after app install **With deferred deep links:** - QR code context preserved through app install - App opens directly to intended content (menu, ticket, offer) - Full campaign tracking and attribution - Higher engagement and conversion rates ## How deferred deep links enable QR code experiences Deferred deep links preserve your QR code target and context even when the app isn't installed at scan time. ### Step 1: Create a QR code with a Redirectly deep link Generate a Redirectly deep link with your content parameters: `https://myapp.redirectly.app/menu?restaurant=downtown&campaign=lunch`. Encode this URL into a QR code. ### Step 2: User scans QR code on mobile If the app is **already installed**, the deep link opens directly to your menu with parameters intact. If the app is **not installed**, Redirectly captures the full URL and stores it. ### Step 3: Redirectly directs to app store User is sent to App Store or Play Store to install your app. The full deep link URL (including context) is **preserved** in Redirectly's servers. ### Step 4: SDK retrieves the deferred link When your app launches for the first time after install, the Redirectly SDK queries the servers: "What deep link should this user see?" Redirectly responds with the full QR code URL including all parameters. ### Step 5: App navigates to content Your app receives the deep link with context (restaurant ID, campaign name, etc.) and navigates directly to the intended content. User sees exactly what they scanned. ## QR codes in restaurants, retail, and events ### Restaurants: Digital Menus A restaurant places QR codes on tables. Customer scans with phone. If they have the app, they see the menu for that specific location. If not, they install the app and immediately see the location-specific menu with real-time updates. Campaign tracking reveals which locations drive the most app installs. ### Retail: In-Store Promotions A clothing retailer prints QR codes on product displays, receipts, or window posters. Customers scan to view product details, reviews, or exclusive in-app offers. Deferred deep linking ensures users who install the app see the exact product they scanned, increasing conversion. UTM parameters track which promotions drive the most app engagement. ### Events: Ticketing & Engagement Event organizers include QR codes on tickets or posters linking to event details, seating maps, or exclusive content. Attendees who don't have the app scan, install, and automatically land on the event page. Track which marketing channels drive the most ticket sales through QR code attribution. ### Marketing: Campaign Attribution Print unique QR codes on different marketing materials (posters, flyers, ads) using Redirectly's link generator. Track which campaigns drive the most app installs and engagement. Preserve campaign context through install with UTM parameters in deep links. ## Simple SDK usage ### Backend: Generate QR code deep links Create Redirectly deep links for your QR codes with context parameters: ```python # Example backend code (Python) import requests import qrcode # Generate a Redirectly deep link for a restaurant menu restaurant_id = 'downtown_location' campaign = 'lunch_promotion' deep_link = ( f"https://myapp.redirectly.app/menu" f"?restaurant={restaurant_id}" f"&campaign={campaign}" ) # Create QR code qr = qrcode.QRCode(version=1, box_size=10) qr.add_data(deep_link) qr.make(fit=True) qr_image = qr.make_image() qr_image.save(f"qr_code_{restaurant_id}.png") ``` ### Mobile: Setup and handle QR code deep links Initialize Redirectly and listen for deep links from QR code scans: ```dart import 'package:redirectly/redirectly.dart'; import 'package:go_router/go_router.dart'; void main() async { // Initialize Redirectly await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Listen for QR code deep links (including deferred) Redirectly.onDeepLink.listen((link) { print('QR code scanned: ${link.fullUrl}'); print('Path: ${link.path}'); print('Parameters: ${link.queryParameters}'); }); runApp(const MyApp()); } ``` ### Navigation: Handle menu deep links Parse QR code parameters and navigate to content: ```dart // In your routing setup Redirectly.onDeepLink.listen((link) { // Parse the QR code parameters if (link.path == 'menu') { final restaurantId = link.queryParameters['restaurant']; final campaign = link.queryParameters['campaign']; // Navigate to restaurant menu with context context.go('/menu?restaurant=$restaurantId&campaign=$campaign'); // Track the campaign analytics.logEvent( 'qr_scan', parameters: { 'restaurant': restaurantId, 'campaign': campaign, }, ); } }); // Route definition GoRoute( path: '/menu', builder: (context, state) { final restaurantId = state.queryParameters['restaurant'] ?? ''; final campaign = state.queryParameters['campaign'] ?? ''; return RestaurantMenuScreen( restaurantId: restaurantId, campaign: campaign, ); }, ) ``` This implementation ensures that: - **QR codes encode Redirectly deep links** with context (restaurant, campaign, product) - **Deferred deep linking preserves context** even if app isn't installed at scan time - **App navigates directly to content** after install without losing context - **Campaign tracking is built-in** via UTM parameters in the URL ## Next steps - [Learn: What is Deferred Deep Linking?](https://redirectly.app/deferred-deep-linking.md) - [Guide: Complete Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [Tool: Free Deep Link Generator](https://redirectly.app/tools/deep-link-generator.md) ## Frequently asked questions ### Can I use the same QR code for both app and web? Yes. Redirectly deep links work seamlessly on both mobile and web. On mobile, the app opens directly. On desktop, the deep link opens your website. This makes QR codes universally useful. ### How do I track which QR codes drive the most installs? Use UTM parameters in your Redirectly deep links (e.g., ?utm_campaign=restaurant_launch). Each QR code can have unique campaign identifiers. Redirectly preserves these through the install, so you can attribute conversions to specific campaigns. ### What happens if someone scans the QR code on desktop? Redirectly detects the platform. On desktop, it opens your mobile website or web app. On mobile without the app installed, it starts the install flow. No friction either way. ### Can I update the deep link destination without reprinting QR codes? Yes. Use a Redirectly short link in the QR code. Then you can update the destination URL in your dashboard anytime without changing the QR code. ### Do QR codes work with React Native? Yes. Redirectly works with Flutter, React Native, and native iOS/Android. The QR code handling is identical across all platforms. ### What data can I pass through a QR code deep link? You can pass any data as query parameters: product IDs, location IDs, promo codes, event IDs, campaign names, etc. Keep URLs under 2048 characters for maximum QR code reliability. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Referral Program Deep Links > Build referral programs that work even when users tap links before installing. Deferred deep links preserve referrer identity through the app install flow. - Canonical: https://redirectly.app/use-cases/referral-programs - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Build referral programs that survive app installs. Use deferred deep links to attribute new users to their referrers, even when they don't have the app installed yet. ## The referral attribution challenge Referral programs drive user growth, but attribution breaks when referral links are tapped before the app is installed. Here's the problem: **What breaks without deferred deep links:** 1. User A refers User B via a referral link: `https://myapp.com/ref/alice123` 2. User B doesn't have the app installed. They tap the link, land on a web page. 3. They're prompted to install. They go to App Store. **Referrer context is lost.** 4. App launches. Your SDK doesn't know who referred User B. Attribution is broken. 5. User A never gets credit for the referral. No reward. Referral loop collapses. **Without deferred deep links:** - Referrer ID lost when user taps link before install - No way to track who invited new user - Rewards can't be distributed correctly - Referral program incentive broken **With deferred deep links:** - Referrer ID preserved across app install - First install attributed to correct referrer - Rewards automatically credited to referrer - Viral growth loop stays intact ## How deferred deep links enable attribution ### Step 1: Generate referral link When User A wants to refer friends, you generate a Redirectly deep link with their referrer ID: `https://myapp.redirectly.app/ref?referrer_id=alice123` ### Step 2: User shares link User A shares the link via SMS, email, or social. User B receives it on mobile and taps it. ### Step 3: Redirectly captures referrer ID If User B has the app: deep link opens directly, referrer ID is passed to your app immediately. If User B doesn't have the app: Redirectly captures the full URL (including referrer_id) and stores it. ### Step 4: App store redirect User B is sent to App Store. Referrer ID is preserved in Redirectly's servers—not lost. ### Step 5: SDK queries deferred link on first launch When your app launches for the first time on User B's device, the Redirectly SDK asks: "What deep link was intended for this device?" Redirectly responds with the referral link including referrer_id. ### Step 6: Credit referrer and award points Your app receives the referrer_id, posts it to your backend: "User B was referred by alice123." Your backend credits User A with the referral reward instantly. ## Implementation with Redirectly ### Backend: Generate referral links When a user requests their referral link, create a Redirectly deep link with their ID: ```javascript # Backend code (Node.js) app.get('/api/get-referral-link', (req, res) => { const userId = req.user.id; const referralLink = ( 'https://myapp.redirectly.app/ref' + '?referrer_id=' + userId + '&campaign=user_referral' ); // Store the referral link in DB for tracking saveReferralLink(userId, referralLink); res.json({ referral_link: referralLink }); }); ``` ### Mobile: Initialize and listen Setup Redirectly and handle referral deep links on first launch: ```dart import 'package:redirectly/redirectly.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Redirectly.initialize( apiKey: 'your_api_key_here', ); // Get deferred link (called on first launch) final deepLink = await Redirectly.getInitialLink(); if (deepLink != null && deepLink.path == 'ref') { final referrerId = deepLink.queryParameters['referrer_id']; print('Referred by: $referrerId'); // Send to backend to credit referrer creditReferral(referrerId); } runApp(const MyApp()); } ``` ### Backend: Credit the referrer When a new user is attributed to a referrer, award points or credits: ```javascript app.post('/api/credit-referral', async (req, res) => { const { referrer_id } = req.body; // Check if referrer exists const referrer = await User.findById(referrer_id); if (!referrer) { return res.status(400).json({ error: 'Invalid referrer' }); } // Award points to referrer referrer.referral_points += 50; // or 5, depending on your rewards referrer.total_referrals += 1; await referrer.save(); // Log the referral for analytics await ReferralEvent.create({ referrer_id, referred_user_id: req.user.id, timestamp: new Date(), reward_amount: 50, }); res.json({ success: true, points_awarded: 50 }); }); ``` ## Complete referral program example ```dart // Flutter app - Handle referral on first launch import 'package:redirectly/redirectly.dart'; import 'package:http/http.dart' as http; class ReferralManager { static Future handleReferralOnFirstLaunch() async { // Get the deferred deep link final deepLink = await Redirectly.getInitialLink(); if (deepLink != null && deepLink.path == 'ref') { final referrerId = deepLink.queryParameters['referrer_id']; if (referrerId != null && referrerId.isNotEmpty) { await _creditReferrer(referrerId); } } } static Future _creditReferrer(String referrerId) async { try { final response = await http.post( Uri.parse('https://api.example.com/api/credit-referral'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ${authToken}', }, body: json.encode({ 'referrer_id': referrerId, }), ); if (response.statusCode == 200) { final data = json.decode(response.body); final pointsAwarded = data['points_awarded']; print('Referrer credited with $pointsAwarded points'); // Show celebration UI to new user showReferralBonus(referrerId, pointsAwarded); } } catch (e) { print('Error crediting referrer: $e'); } } } // User profile - Show referral link class ReferralLinkWidget extends StatefulWidget { final User user; const ReferralLinkWidget({required this.user}); @override State createState() => _ReferralLinkWidgetState(); } class _ReferralLinkWidgetState extends State { String? referralLink; bool isLoading = true; @override void initState() { super.initState(); _fetchReferralLink(); } Future _fetchReferralLink() async { try { final response = await http.get( Uri.parse('https://api.example.com/api/get-referral-link'), headers: { 'Authorization': 'Bearer ${authToken}', }, ); if (response.statusCode == 200) { final data = json.decode(response.body); setState(() { referralLink = data['referral_link']; isLoading = false; }); } } catch (e) { setState(() => isLoading = false); } } Future _copyToClipboard() async { if (referralLink != null) { await Clipboard.setData(ClipboardData(text: referralLink!)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Referral link copied!')), ); } } Future _shareLink() async { if (referralLink != null) { await Share.share( 'Join me on the app! $referralLink', subject: 'Join my community', ); } } @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Your Referral Link', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), if (isLoading) const CircularProgressIndicator() else if (referralLink != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: SelectableText( referralLink!, style: const TextStyle(fontSize: 12), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _copyToClipboard, icon: const Icon(Icons.copy), label: const Text('Copy'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: _shareLink, icon: const Icon(Icons.share), label: const Text('Share'), ), ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.info, size: 16), const SizedBox(width: 8), Expanded( child: Text( 'Earn 50 points for each friend who joins!', style: TextStyle(fontSize: 12, color: Colors.blue[900]), ), ), ], ), ), ] else const Text('Failed to load referral link'), ], ), ), ); } } ``` This example includes: - **Deferred link detection** on first launch - **API call to backend** to credit the referrer - **Referral link generator** in user profile - **Share and copy buttons** for easy distribution ## Next steps - [Read: Referral Program Deep Linking Guide](https://redirectly.app/blog/referral-program-deep-linking.md) - [Learn: What is Deferred Deep Linking?](https://redirectly.app/deferred-deep-linking.md) - [Guide: Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) ## Frequently asked questions ### What if the referrer ID is invalid? On your backend, validate the referrer ID against your users table before awarding points. If invalid, simply don't credit anything. The new user still gets onboarded normally. ### Can I track multiple referrals per user? Yes. Each time a deferred link is detected with a referrer_id, you can log a new ReferralEvent. Track all historical referrals in your database. ### What rewards should I offer? Common approaches: 50-500 points per referral, premium features unlocked, or cash/credit rewards. The amount depends on your unit economics and user value. ### How do I prevent referral fraud? Validate that the new user is not already in your system. Check their IP/device ID for obvious duplicates. Consider requiring email verification before awarding points. ### Do I need to handle this differently for Android vs iOS? No. The Redirectly SDK handles the platform differences. Your code is the same for both. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Universal Links Not Working? Fix iOS Deep Links > Fix iOS universal links with this troubleshooting guide. Debug AASA file issues, entitlements, Associated Domains, CDN caching, and simulator limits. - Canonical: https://redirectly.app/debug/universal-links-not-working - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Comprehensive troubleshooting guide to debug and fix iOS universal links issues. Learn about AASA file validation, Associated Domains configuration, CDN caching, and testing strategies. ## Why universal links fail on iOS ### Universal links fall back to web browser If your universal links open in Safari instead of your app, the AASA file is not validating or the app is not installed. This is Apple's fallback behavior. iOS universal links require a perfect configuration of three components: the AASA file, app entitlements, and domain validation. If any one is wrong, the entire system fails silently. ### AASA File Apple-app-site-association must be valid JSON, properly hosted, and correctly formatted with your app details. ### Entitlements Your Xcode project must have Associated Domains capability enabled with proper domain configuration. ### Domain Validation Apple must be able to fetch and validate your AASA file before the app can open universal links. ## Step-by-step troubleshooting ### 1. Validate your AASA file Use our free AASA validator to check for JSON errors, content-type issues, and configuration problems. - Copy your AASA file content into the validator at /aasa-validator - Check that JSON is valid and properly formatted - Verify content-type is application/json (not text/plain) - Ensure applinks key is present with correct bundle ID and team ID ### 2. Check your Xcode entitlements Associated Domains capability must be enabled and properly configured in your signing settings. - Open Xcode > Select target > Signing & Capabilities - Click + Capability and add Associated Domains - Enter domain(s) in format: `applinks:yourdomain.com` - For subdomains, use: `applinks:*.yourdomain.com` - Rebuild and resign your app with updated entitlements ### 3. Test on a real iOS device The iOS Simulator does not validate AASA files or entitlements. Testing on Simulator masks real issues. - Build and deploy your app to a physical iPhone or iPad - Delete the app completely from the device (long press > Remove App > Delete App) - Wait 1-2 minutes for AASA cache to clear - Reinstall the app from Xcode or TestFlight - Test the universal link on the real device ### 4. Verify AASA file is accessible Apple must be able to fetch your AASA file without redirects or authentication barriers. - Test in browser: https://yourdomain.com/.well-known/apple-app-site-association - Should return JSON, not 404, 403, or redirect - Check CDN headers: Content-Type should be application/json - Verify no authentication or IP restrictions block Apple servers - If using redirects, keep to 5 redirects maximum ### 5. Check for CDN caching issues CDN caching can prevent AASA updates from being deployed. Some CDNs require explicit cache purging. - Check CDN cache settings for /.well-known/apple-app-site-association - Set cache-control headers to low TTL during development (e.g., 300 seconds) - Purge CDN cache after AASA file updates - Consider hosting AASA directly on origin (no cache) while debugging - Verify Cloudflare, Fastly, or other CDN is not caching the file ## Common causes of universal link failures ### AASA file not valid JSON **Cause:** Syntax errors, trailing commas, or missing quotes in apple-app-site-association **Fix:** Use the AASA validator to identify JSON errors and correct syntax ### Wrong content-type header **Cause:** AASA served as text/plain instead of application/json **Fix:** Configure your web server or CDN to serve with Content-Type: application/json ### Team ID or bundle ID mismatch **Cause:** Values in AASA file do not match your app entitlements exactly **Fix:** Verify team ID from Apple Developer Portal and bundle ID from Xcode ### Associated Domains not enabled in Xcode **Cause:** Capability not added to app signing settings **Fix:** Add Associated Domains capability in Xcode > Signing & Capabilities ### Testing on iOS Simulator **Cause:** Simulator does not validate AASA or entitlements **Fix:** Always test on a real device during development and troubleshooting ### Aggressive CDN caching **Cause:** CDN caching the AASA file, preventing updates from being deployed **Fix:** Purge cache after AASA updates or set low TTL during testing ### Too many redirects **Cause:** AASA file requires more than 5 redirects to reach final destination **Fix:** Host AASA directly at /.well-known/apple-app-site-association or minimize redirects ### Subdomain configuration **Cause:** Wildcard patterns or specific subdomains not matching links **Fix:** Use `applinks:*.domain.com` for all subdomains or specific subdomains individually ## Universal links fix checklist - AASA file is valid JSON (validate with /aasa-validator) - Content-Type header is application/json - AASA file is accessible at /.well-known/apple-app-site-association - Team ID in AASA matches Apple Developer Portal account - Bundle ID in AASA matches Xcode project exactly - Associated Domains capability is enabled in Xcode - Associated Domains entries are in format `applinks:domain.com` - App is signed with correct provisioning profile - Testing on real iOS device (not Simulator) - App has been completely deleted and reinstalled after changes - CDN cache has been purged after AASA updates - No redirect chains exceed 5 hops - No authentication or IP restrictions block Apple servers - Tested on at least iOS 14+ (universal links support) ## Debug universal links with Redirectly Use Redirectly's free AASA validator to test your configuration and get detailed error messages: - [Test your AASA file](https://redirectly.app/aasa-validator.md) - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) ## Frequently asked questions ### Why do universal links work in Simulator but not on device? Simulator never validates AASA files or entitlements. Always test on real hardware. Simulator testing provides a false sense of success. ### How long does it take for AASA changes to take effect? Apple caches AASA for up to 24 hours. Delete and reinstall the app to refresh the cache immediately. For real devices, you can also force refresh by deleting app and reinstalling. ### What if my app opens the link but in the wrong view? This means universal links are working, but your app is not handling the deep link data correctly. Check your AppDelegate or SwiftUI scene handling for the Userinfo payload. ### Can I use universal links with multiple domains? Yes. Add multiple domains to Associated Domains (`applinks:domain1.com`, `applinks:domain2.com`) and create an AASA file for each domain. ### Do I need both AASA file and custom URL scheme? No. Universal links are better. URL schemes are fallbacks. Use universal links for all modern iOS apps. URL schemes only if you support iOS 8 or earlier. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Android App Links Not Opening? Fix Deep Links > Fix Android app links not opening with this guide. Troubleshoot SHA-256 fingerprints, assetlinks.json errors, autoVerify, and Digital Asset Links setup. - Canonical: https://redirectly.app/debug/android-app-links-not-opening - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete troubleshooting guide for Android app links failures. Learn about SHA-256 fingerprints, assetlinks.json configuration, autoVerify, Play App Signing, and Digital Asset Links verification. ## Why Android app links fail ### App links open in browser, not app If links open Chrome instead of your app, the assetlinks.json file is not validating or SHA-256 does not match. Without autoVerify, users see app chooser dialog. Android app links require three critical components: correct SHA-256 fingerprint, proper assetlinks.json hosting, and autoVerify enabled. A single mismatch breaks the entire system. ### SHA-256 Fingerprint Must match exactly from your signing key. Use Play Console version if using Play App Signing. ### assetlinks.json Must be valid JSON, properly hosted at .well-known/, and accessible to Google servers. ### autoVerify Intent filter attribute that enables automatic verification when app installs. ## Step-by-step troubleshooting ### 1. Get your correct SHA-256 fingerprint Critical: Use the fingerprint from Google Play Console, not your local keystore. - If using Play App Signing: Go to Google Play Console > App signing for internal testing - Copy the SHA-256 certificate fingerprint shown in the console - If self-signing: Run `keytool -list -v -keystore keystore.jks | grep SHA256` - Format must be pairs separated by colons: AA:BB:CC:DD... (no spaces) - Save this value to use in assetlinks.json ### 2. Create and validate assetlinks.json Your JSON must be valid and include the correct SHA-256 fingerprint. - Create file .well-known/assetlinks.json in your web server root - Use format: `[{"relation":["delegate_permission/common.handle_all_urls"],"target":{"namespace":"android_app","package_name":"com.example.app","sha256_cert_fingerprints":["AA:BB:CC:..."]}}]` - Validate with /assetlinks-validator before deploying - Check for trailing commas, missing quotes, or other JSON syntax errors - Test in browser: domain.com/.well-known/assetlinks.json should return JSON ### 3. Add autoVerify to intent filter This attribute enables automatic verification when your app installs. - Open your AndroidManifest.xml - Find the intent-filter for deep links - Add: `android:autoVerify="true"` to the intent-filter tag - Example: `` - Rebuild and test your app after making this change ### 4. Verify hosting and accessibility Google and Android must be able to fetch your assetlinks.json without restrictions. - Test file is accessible: `curl -I https://yourdomain.com/.well-known/assetlinks.json` - Should return 200 OK with Content-Type: application/json - Verify no authentication, IP restrictions, or WAF blocks Google servers - Check for redirect chains (Google can follow up to 5 redirects) - Ensure DNS resolves correctly and certificate is valid ### 5. Verify in Google Play Console Google Play Console shows verification status and issues. - Go to App Links section in Google Play Console - Check "Digital Asset Links" verification status - If verification fails, follow error messages to identify issues - Common errors: wrong SHA-256, assetlinks.json not found, invalid format - After fixing, re-test verification (may take up to 2 hours) ## Common causes of app link failures ### Wrong SHA-256 fingerprint **Cause:** Using local keystore fingerprint when Play App Signing is enabled **Fix:** Get SHA-256 from Google Play Console > App signing, not keytool output ### assetlinks.json not found (404) **Cause:** File is not at .well-known/assetlinks.json or web server not configured **Fix:** Verify file location and test with curl. Ensure web server serves from correct root. ### Invalid JSON in assetlinks.json **Cause:** Syntax errors like trailing commas, missing quotes, or formatting issues **Fix:** Use /assetlinks-validator to identify JSON errors before deploying ### Wrong content-type header **Cause:** assetlinks.json served as text/plain instead of application/json **Fix:** Configure web server (nginx, Apache, CDN) to serve with application/json ### autoVerify not in intent-filter **Cause:** Attribute missing from AndroidManifest.xml **Fix:** Add `android:autoVerify="true"` to your deep link intent-filter ### assetlinks.json changes not deployed **Cause:** CDN caching or web server not updated with new file **Fix:** Purge CDN cache and verify new file is live before testing ### Package name mismatch **Cause:** assetlinks.json contains different package name than your app **Fix:** Verify package_name in assetlinks.json exactly matches AndroidManifest.xml ### Fingerprint has spaces or lowercase **Cause:** Formatting issues in SHA-256 value **Fix:** Ensure uppercase hex with colons between pairs: AA:BB:CC:DD (no spaces) ## Android app links fix checklist - SHA-256 fingerprint copied from Google Play Console (or keytool for local testing) - assetlinks.json is valid JSON (test with /assetlinks-validator) - assetlinks.json located at /.well-known/assetlinks.json on web server - assetlinks.json is accessible via curl or browser (200 OK, not 404) - Content-Type header is application/json (not text/plain) - Package name in assetlinks.json matches AndroidManifest.xml exactly - SHA-256 fingerprint format is correct (uppercase, colon-separated pairs) - `android:autoVerify="true"` is in your deep link intent-filter - AndroidManifest.xml has proper intent-filter with action, category, and data tags - App is signed with correct signing key matching SHA-256 - App has been uninstalled and reinstalled after code changes - CDN cache has been purged after assetlinks.json updates - Google Play Console Digital Asset Links verification shows success - App is built in release mode (if testing production verification) ## Debug Android app links with Redirectly Use Redirectly's free assetlinks.json validator to test your configuration and identify issues: - [Validate assetlinks.json](https://redirectly.app/assetlinks-validator.md) - [React Native Deep Linking Guide](https://redirectly.app/react-native-deferred-deep-linking.md) ## Frequently asked questions ### What is the difference between Play App Signing and local keystore? Google Play Console can re-sign your app with a different key (Play App Signing). This creates a different SHA-256 fingerprint than your local key. Always use Play Console fingerprint for production verification. ### Can I test app links before uploading to Play Store? Yes. Test locally with your local keystore SHA-256, or use Play Console's internal testing track which shows the actual Play App Signing fingerprint. ### How long does app link verification take? Android verifies assetlinks.json when the app installs. If verification fails, users see app chooser dialog instead of automatic open. Fixing and reinstalling should work immediately. ### Can I have app links for multiple domains? Yes. Create assetlinks.json on each domain, and add multiple data entries in your AndroidManifest.xml intent-filter. Each domain needs its own assetlinks.json file. ### What if my app uses App Links but users still see chooser dialog? autoVerify may be missing, or app is not installed with correct signing key. Check Android System Settings > Apps > Your App > Supported Links for verification status. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deferred Deep Link Not Firing After Install? > Fix deferred deep links that fail after app install. Troubleshoot SDK initialization, clipboard access, Play Install Referrer, and real-device testing. - Canonical: https://redirectly.app/debug/deferred-deep-link-not-firing - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete troubleshooting guide for deferred deep link failures. Learn about SDK initialization, clipboard access, Play Install Referrer, real device testing, and dashboard debugging. ## Why deferred deep links fail after installation ### Link opens but deep link data is not passed If your app opens but doesn't navigate to the intended deep link destination, the attribution data failed to transmit from the link to your app on install. Deferred deep links require data to survive the app install process. This depends on SDK initialization timing, platform-specific referral mechanisms, clipboard access, and proper handling in your app code. ### SDK Init SDK must initialize before the app renders. Delayed init can cause referral data to be lost. ### Referrer Source iOS uses clipboard; Android uses Play Install Referrer. Both must be properly configured. ### Callback Handler App must listen for and handle deep link data callbacks from the SDK. ## Step-by-step troubleshooting ### 1. Initialize SDK at app startup SDK must initialize before any views render or callbacks are registered. - Call Redirectly.init() or equivalent SDK method in your main app widget/activity - For Flutter: Call in main() before runApp(). For React Native: Call in App.js before rendering. - For native iOS: Call in AppDelegate application(_:didFinishLaunchingWithOptions:) - For native Android: Call in MainActivity onCreate() or Application onCreate() - Verify init is not deferred by loading screens, state managers, or async operations ### 2. Request clipboard permission on iOS Redirectly uses clipboard to pass deferred link data. Request permission in Info.plist. - Add to Info.plist: NSPasteboardUsageDescription or NSPrivacyAccessedAPITypes entry - Include purpose: "To detect and attribute app installs from links" - User will see permission prompt on first app open - Without permission, fallback mechanisms still work but attribution may be limited - Grant permission when prompted for best results ### 3. Enable Play Install Referrer on Android Android needs Google Play Services for referral data. Configure your build.gradle. - Add to build.gradle: `implementation 'com.google.android.gms:play-services-ads:...'` or play-services-basement - Redirectly SDK will automatically use Play Install Referrer API - Verify referrer is passed: Check Redirectly dashboard logs - Test on real device with Google Play Store installed - Emulator with Play Services may not properly simulate referrer behavior ### 4. Test on real device with proper flow Emulators do not properly simulate deferred deep link behavior. Real devices required. - Fully delete your app from the device (Settings > Apps > Uninstall) - Open a link from Safari (iOS) or Chrome (Android) pointing to your Redirectly link - If prompted, choose "App Store" / "Play Store" - do NOT open the link yet - Install your app from App Store or Play Store - Launch the app - if configured correctly, you should see the deferred deep link data ### 5. Monitor with Redirectly dashboard and app logs Verify link tracking and data delivery via logs and dashboard analytics. - Go to Redirectly dashboard and create a test link - Enable debug logging in your app (check SDK documentation) - Add console.log or print statements in your deep link callback handler - View log output to see if data is received from SDK - Check Redirectly dashboard for link clicks, installs, and conversion events ## Common causes of deferred deep link failures ### SDK not initialized at startup **Cause:** Init called too late or after app views render **Fix:** Move SDK init to app entry point before any UI rendering ### No callback handler registered **Cause:** App not listening for deep link data from SDK **Fix:** Register deep link callback handler in your SDK init or setup code ### Testing on emulator/simulator **Cause:** Emulators do not simulate referrer APIs or clipboard correctly **Fix:** Always test on real device. Emulator may show false negatives. ### No clipboard permission on iOS **Cause:** User denied or app did not request clipboard access **Fix:** Request UIPasteboard permission in Info.plist and prompt user ### Play Install Referrer not configured on Android **Cause:** Google Play Services not included in app dependencies **Fix:** Add Google Play Services to build.gradle and rebuild ### Testing by re-opening the app (not install from store) **Cause:** Deferred deep links only fire on fresh install, not app update **Fix:** Fully delete and reinstall app from App Store or Play Store ### Deep link data callback throws error **Cause:** Callback handler has syntax errors or crashes **Fix:** Check app logs for exceptions in deep link handler ### Deep link bundle ID or app ID incorrect in Redirectly **Cause:** Configuration in Redirectly dashboard does not match app **Fix:** Verify bundle ID in Redirectly matches exactly your app bundle ID ## Deferred deep link fix checklist - SDK initialization called at app startup before any views render - Clipboard permission requested on iOS (Info.plist configured) - Deep link callback handler is registered with the SDK - Callback handler has no syntax errors or exceptions - Google Play Services included in Android build.gradle - Testing on real device (not emulator or simulator) - App completely deleted before each test install - Link clicked from Safari (iOS) or Chrome (Android) - App installed fresh from App Store or Play Store - Deep link data expected after first app launch - Redirectly dashboard shows link clicks and install events - App logs show deep link data callbacks executing - App bundle ID matches Redirectly dashboard configuration - No network or permission restrictions blocking Redirectly API calls ## Debug deferred deep links with Redirectly Use Redirectly's dashboard to track link clicks, app installs, and verify deferred deep link delivery: - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native Deferred Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) ## Frequently asked questions ### Why does deferred deep link work in development but not in production? Ensure your production app is signed with the same key as your development build. Different signing keys result in different bundle/package identifiers that Redirectly cannot recognize. ### Can I test deferred deep links on the same device? Not really - deferred deep links require the app to be completely uninstalled. Testing on the same device requires full delete and reinstall from app store, which is slow. Use multiple test devices. ### What if my app handles deep links correctly but Redirectly does not show conversions? Check Redirectly dashboard for link clicks and install events. If clicks show but installs do not, the referrer mechanism may not be working. Verify Play Install Referrer on Android or clipboard on iOS. ### How long does deferred deep link data persist? Deferred link data typically persists for 24-48 hours after app install. After that window, the link data expires and the app will not receive deep link information. ### Can I use deferred deep links with user login? Yes. Store the deep link data in your app state or preferences. After user logs in, navigate to the saved deep link destination. This is useful for onboarding flows. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # AASA File Errors & How to Fix Them > Fix common AASA file errors: invalid JSON, wrong content-type, CDN and redirect issues, wildcard patterns, missing applinks key, and team ID confusion. - Canonical: https://redirectly.app/debug/aasa-file-errors - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete guide to AASA file errors, validation, and fixes. Learn about JSON syntax, content-type headers, team ID and bundle ID configuration, wildcard patterns, and CDN issues. ## Understanding AASA file structure ### Minimal valid AASA file ```json { "applinks": { "apps": [], "details": [ { "appID": "TEAM-ID.com.example.app", "paths": ["/path/*"] } ] } } ``` ### appID Format The appID must be: `TEAM-ID.BUNDLE-ID` - **TEAM-ID:** Found in Apple Developer Portal (10-character identifier) - **BUNDLE-ID:** Your app's bundle identifier from Xcode (e.g., com.example.app) - **Example:** 9ABC12345D.com.example.app ### Paths Configuration Specify which URL paths should trigger your app: - `/path/*` - Matches any URL starting with /path/ - `/*` - Matches all paths on the domain - `/path/to/page` - Matches exact path only - `NOT paths: ["/path/*"]` - Excludes specific paths ### Hosting Location AASA file must be hosted at: `https://yourdomain.com/.well-known/apple-app-site-association` No file extension. Must be served with `Content-Type: application/json` ## Common AASA errors and how to fix them ### Unexpected token } in JSON **Cause:** Trailing comma in array or object **Example:** `"details": [ { ... }, ] // Wrong!` **Fix:** Remove trailing commas before closing brackets and braces ### Expected property name or '}' **Cause:** Missing quotes around JSON keys **Example:** `{ applinks: {} } // Should be "applinks"` **Fix:** Wrap all JSON keys in double quotes: "applinks" ### Invalid appID format **Cause:** Team ID or bundle ID incorrect or formatted wrong **Example:** `"appID": "9ABC12345Dcom.example.app" // Missing dot!` **Fix:** Use format Team-ID.Bundle-ID (e.g., 9ABC12345D.com.example.app) ### Content-Type is text/plain **Cause:** Web server configured to serve AASA as text file **Example:** `curl -I shows Content-Type: text/plain` **Fix:** Configure web server to serve with Content-Type: application/json. Use .htaccess for Apache or nginx config for nginx. ### File not found (404) **Cause:** AASA file path is wrong or file does not exist **Example:** `File at /app/aasa or /apple-app-site-association` **Fix:** Place file at /.well-known/apple-app-site-association in web root. Test with curl or browser. ### Missing "applinks" key **Cause:** JSON structure missing top-level applinks object **Example:** `{ "details": [] } // Missing applinks!` **Fix:** Wrap all content in "applinks" object with nested "details" array ### Paths array is empty **Cause:** No paths specified or paths configured wrong **Example:** `"paths": [] // Will never match any links!` **Fix:** Add path patterns: "paths": ["/path/*"] or "paths": ["/*"] ### Redirect chain too long **Cause:** AASA location redirects more than 5 times **Example:** `domain.com -> cdn1.com -> cdn2.com -> ... (6+ hops)` **Fix:** Host AASA directly at /.well-known/ or limit redirects to 5 maximum ### Team ID doesn't match Apple Developer account **Cause:** Using wrong team ID from different account **Example:** `"appID": "1234567890.com.example.app" // Wrong team ID` **Fix:** Get correct Team ID from Apple Developer Portal > Account tab ## How to validate your AASA file ### 1. Use Redirectly AASA Validator - Go to https://redirectly.app/aasa-validator - Paste your AASA file content into the text area - Click "Validate" - Review error messages and fix issues ### 2. Test in your browser - Open https://yourdomain.com/.well-known/apple-app-site-association - Should see JSON content, not 404 or error - Should return 200 OK status - Verify Content-Type is application/json in response headers ### 3. Check with curl command - Run: `curl -I https://yourdomain.com/.well-known/apple-app-site-association` - Look for: HTTP/2 200 or HTTP/1.1 200 OK - Look for: Content-Type: application/json - No 301/302 redirects should be present ### 4. Validate JSON syntax - Use online JSON validator (jsonlint.com) - Paste your AASA file content - Look for syntax errors reported - Fix any trailing commas, missing quotes, or bracket mismatches ### 5. Verify team ID and bundle ID - Go to Apple Developer Portal - Account > Membership > Team ID (10 chars) - Your app in Xcode > General > Bundle Identifier - Verify appID in AASA is: TEAM-ID.BUNDLE-ID ## Quick validation with Redirectly Paste your AASA file and get instant validation with detailed error messages: - [Open AASA Validator](https://redirectly.app/aasa-validator.md) ## Frequently asked questions ### Can I use .json file extension instead of no extension? No. Apple requires the file to be at /.well-known/apple-app-site-association with no file extension. The content-type header tells Apple it's JSON. ### Can I host AASA on a subdomain instead of root domain? Yes. You can host AASA on a subdomain if your universal links point to that subdomain. Each domain needs its own AASA file. ### Do I need different AASA files for different app versions? No. One AASA file per domain works for all versions of your app. Apple caches it for 24 hours, so updates take time to propagate. ### What happens if my AASA file is invalid? iOS will fall back to Safari. Users will see the webpage instead of your app opening. This is why validation is critical. ### Can I wildcard all subdomains with applinks? Yes, use `"paths": ["/*"]` to match all paths on the domain. For subdomains, you can use specific subdomains or a single wildcard AASA file. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # assetlinks.json Errors & How to Fix Them > Fix assetlinks.json errors: wrong SHA-256 fingerprint, Play App Signing, package name mismatch, hosting issues, and debug vs release certificates. - Canonical: https://redirectly.app/debug/assetlinks-json-errors - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete guide to assetlinks.json errors, validation, and fixes. Learn about SHA-256 fingerprints, Play App Signing, package names, hosting, and multiple fingerprint configurations. ## Understanding assetlinks.json structure ### Minimal valid assetlinks.json ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": ["AA:BB:CC:DD:..."] } } ] ``` ### File Location & Hosting Must be hosted at: `https://yourdomain.com/.well-known/assetlinks.json` - **Content-Type:** application/json (not text/plain) - **HTTP Status:** 200 OK (not 301, 302, or 404) - **Accessibility:** Publicly accessible, no authentication required ### Key Fields Explained - **relation:** Must be: "delegate_permission/common.handle_all_urls" for app links - **namespace:** Must be: "android_app" for Android app links - **package_name:** Your app's package name from AndroidManifest.xml (e.g., com.example.app) - **sha256_cert_fingerprints:** Array of SHA-256 fingerprints in uppercase, colon-separated format ## Understanding SHA-256 fingerprints ### Critical distinction: Play App Signing vs Local Keystore If you upload unsigned APKs to Google Play Console, Google re-signs your app with a different key. This creates a **different SHA-256 fingerprint** than your local keystore. Always use the Play Console fingerprint. ### How to get SHA-256 fingerprint #### Option 1: Google Play Console (Recommended) 1. Go to Google Play Console 2. Select your app 3. Go to Release > Internal testing (or Production) 4. Look for "App signing for internal testing" or "Google Play App Signing" 5. Copy the SHA-256 certificate fingerprint 6. Format: AA:BB:CC:DD:... (uppercase, colon-separated) #### Option 2: Using keytool (Local Testing Only) Only if you self-sign and are not using Play App Signing: ```bash keytool -list -v -keystore keystore.jks | grep SHA256 ``` ### SHA-256 Format Requirements - **Uppercase:** AA:BB:CC (not aa:bb:cc) - **Colon-separated:** AA:BB:CC:DD (not AABBCCDD) - **No spaces:** AA:BB (not AA: BB) - **In array:** ["AA:BB:CC:..."] with square brackets ### Multiple Fingerprints You can add multiple fingerprints if your app is signed with different keys: ```json "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:...", // Release key "11:22:33:44:55:66:...", // Debug key "AA:AA:AA:AA:AA:AA:..." // Other signing key ] ``` Useful for supporting both debug and release builds, or multiple developer keys. ## Common assetlinks.json errors and fixes ### SHA-256 fingerprint does not match **Cause:** Using local keytool fingerprint when Play App Signing is enabled in Google Play Console **Fix:** Get SHA-256 from Google Play Console > App signing section, not from keytool ### assetlinks.json not found (404) **Cause:** File is not at /.well-known/assetlinks.json or web server not configured **Fix:** Verify file location and test with curl. Ensure web server serves from correct root directory. ### Package name mismatch **Cause:** package_name in assetlinks.json does not match AndroidManifest.xml **Fix:** Verify package_name in assetlinks.json exactly matches `` in AndroidManifest.xml ### Content-Type is text/plain **Cause:** Web server serving assetlinks.json as plain text instead of JSON **Fix:** Configure web server to serve /.well-known/assetlinks.json with Content-Type: application/json ### Invalid JSON syntax **Cause:** Trailing commas, missing quotes, or mismatched brackets **Fix:** Use /assetlinks-validator to check JSON syntax. Remove trailing commas, ensure all keys are quoted. ### SHA-256 has lowercase letters **Cause:** Fingerprint formatted in lowercase instead of uppercase **Fix:** Convert all SHA-256 characters to uppercase: aa:bb:cc → AA:BB:CC ### SHA-256 has spaces **Cause:** Fingerprint contains spaces instead of being compact **Fix:** Remove all spaces: AA: BB: CC → AA:BB:CC ### Wrong relation type **Cause:** relation field set to something other than delegate_permission/common.handle_all_urls **Fix:** Use exactly: `"relation": ["delegate_permission/common.handle_all_urls"]` ### CDN caching assetlinks.json **Cause:** Changes to assetlinks.json not reflected because CDN is caching old version **Fix:** Purge CDN cache or test with curl. Disable caching for /.well-known/ paths during development. ### App signed with different key than assetlinks.json **Cause:** App APK signed with key A, but assetlinks.json has fingerprint from key B **Fix:** Ensure APK is signed with the same key whose fingerprint is in assetlinks.json ## Quick validation with Redirectly Paste your assetlinks.json and get instant validation with detailed error messages: - [Open assetlinks Validator](https://redirectly.app/assetlinks-validator.md) ## Frequently asked questions ### I have both debug and release builds. What SHA-256 do I use? Add both to the sha256_cert_fingerprints array. This way both debug and release builds can use app links. Use the release fingerprint for production app links. ### Where do I get my package name? Open AndroidManifest.xml and look for: ``. The package_name in assetlinks.json must match exactly, including capitalization. ### How long does assetlinks.json verification take? Google Play Console usually verifies within minutes, but can take up to 2 hours. After verification, app links work automatically when app installs from Play Store. ### Can I test assetlinks.json locally before deploying? Yes. Use your local keytool SHA-256 for testing, then swap to the Play Console fingerprint before publishing. Or test on internal testing track in Play Console. ### What if my SHA-256 fingerprint changed? If you changed signing keys, update the sha256_cert_fingerprints array with the new fingerprint. Old fingerprint will stop working. Keep old fingerprints if you need to support old app versions. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly Integrations > Connect Redirectly deferred deep linking with Expo, Supabase, FlutterFlow, Segment, Amplitude, Mixpanel, OneSignal, and Firebase using step-by-step guides. - Canonical: https://redirectly.app/integrations - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Deep linking works better with your entire stack. Discover how to integrate Redirectly with frameworks, analytics, authentication, and engagement tools. ## Choose your integration Whether you're building with Expo, using FlutterFlow, or running complex analytics, Redirectly integrates seamlessly with your existing tools. ### Frameworks - [Expo](https://redirectly.app/integrations/expo.md) — Deferred deep linking for Expo apps. Configure app.json, set up EAS Build, and handle deep links with TypeScript. (Tags: Expo, React Native, EAS Build) ### Authentication - [Supabase](https://redirectly.app/integrations/supabase.md) — Email verification and magic links for Flutter with Supabase authentication and Redirectly deferred deep linking. (Tags: Supabase, Email Verification, Magic Links) ### No-Code - [FlutterFlow](https://redirectly.app/integrations/flutterflow.md) — No-code deep linking for FlutterFlow apps. Configure custom actions and navigation without writing code. (Tags: FlutterFlow, No-Code, Custom Actions) ### Analytics - [Segment](https://redirectly.app/integrations/segment.md) — Send install attribution and deep link events to Segment. Connect to 200+ destinations. (Tags: Segment, Attribution, Analytics) - [Amplitude](https://redirectly.app/integrations/amplitude.md) — Track install attribution and build cohorts by campaign. Measure retention and LTV by traffic source. (Tags: Amplitude, Cohorts, Retention) - [Mixpanel](https://redirectly.app/integrations/mixpanel.md) — Behavioral analytics with attribution. Track user funnels and paths by install source. (Tags: Mixpanel, Funnels, Behavior) ### Engagement - [OneSignal](https://redirectly.app/integrations/onesignal.md) — Deep-linked push notifications. Send users directly to relevant app screens from OneSignal campaigns. (Tags: OneSignal, Push Notifications, Engagement) ### Backend - [Firebase](https://redirectly.app/integrations/firebase.md) — Replace Firebase Dynamic Links with Redirectly. Keep Firebase for auth, database, and analytics. (Tags: Firebase, Alternative, Migration) ## Missing an integration? Redirectly works with any mobile framework and analytics platform via REST API. Check our documentation or reach out to learn about your specific use case. - [Get in touch](https://redirectly.app/contact) - [View documentation](https://redirectly.app/flutter-deferred-deep-linking.md) ## Learn deep linking fundamentals New to deep linking? Start with our comprehensive guides to understand deferred deep linking, attribution, and best practices. - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [Deferred Deep Linking](https://redirectly.app/deferred-deep-linking.md) - [View all blog posts](https://redirectly.app/blog.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Expo Deep Linking with Redirectly > Add deferred deep linking to Expo apps with Redirectly. Configure deep links in app.json, set up EAS Build, and handle link events with TypeScript examples. - Canonical: https://redirectly.app/integrations/expo - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete guide to implementing deferred deep linking in Expo apps. Learn how to configure app.json, set up EAS Build, and handle deep link events with TypeScript code examples. ## How Redirectly works with Expo Redirectly provides deferred deep linking for Expo apps by handling the link resolution server-side. When users click a Redirectly link before your app is installed, Redirectly tracks the install attribution and delivers the deep link to your app on first launch—no native code required. ### Why Expo developers choose Redirectly - **No native code:** Works with Expo Go and EAS builds - **Deferred linking:** Capture installs and route users to the right screen - **Real-time analytics:** Track which links drive installs and usage - **Custom subdomains:** Use your own domain for branded links ### Typical implementation flow 1. User clicks a Redirectly link before installing your app 2. Redirectly captures the install event and stores the deep link data 3. User installs and launches your app 4. Your app fetches deep link data from Redirectly and navigates accordingly ## Configure app.json for deep linking First, configure your app.json to handle deep links. This tells Expo which domain scheme to listen for. ```jsonc // app.json { "expo": { "name": "MyApp", "slug": "myapp", "scheme": "myapp", "plugins": [ [ "expo-redirect-scheme", { "scheme": "myapp", "prefix": "https://myapp.redirectly.app/" } ] ] } } ``` Replace `myapp` with your app name and `myapp.redirectly.app` with your Redirectly subdomain. ## EAS Build setup for production When using EAS Build, you need to configure deep linking for both iOS and Android using eas.json and native configuration. ```text // eas.json { "build": { "production": { "ios": { "buildType": "app-store", "scheme": "myapp" }, "android": { "buildType": "app-bundle" } } } } // ios/Podfile - add to post_install post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', 'REDIRECT_SCHEME=myapp://' ] end end end ``` ## Handle deep links with TypeScript Set up event listeners to handle deep links when your app launches or is already running. ```typescript // hooks/useDeepLinking.ts import { useEffect } from 'react' import * as Linking from 'expo-linking' interface DeepLinkData { path: string params: Record } export function useDeepLinking( onDeepLink: (data: DeepLinkData) => void ) { useEffect(() => { // Handle initial URL (app launched from cold state) const getInitialURL = async () => { const url = await Linking.getInitialURL() if (url != null) { const { hostname, path, queryParams } = Linking.parse(url) onDeepLink({ path: path || '/', params: queryParams || {} }) } } getInitialURL() // Handle URL when app is already running const listener = Linking.addEventListener( 'url', ({ url }) => { const { path, queryParams } = Linking.parse(url) onDeepLink({ path: path || '/', params: queryParams || {} }) } ) return () => listener.remove() }, [onDeepLink]) } ``` Then use this hook in your main app component to handle navigation: ```tsx // App.tsx import { useDeepLinking } from './hooks/useDeepLinking' import { NavigationContainer } from '@react-navigation/native' export default function App() { useDeepLinking(({ path, params }) => { // Navigate to the deep linked screen console.log('Deep link received:', { path, params }) // Example: product/123 → navigate to ProductScreen if (path?.startsWith('product/')) { const productId = path.split('/')[1] navigation.navigate('ProductScreen', { id: productId }) } }) return ( {/* Your app navigation */} ) } ``` ## Key benefits & best practices ### Trackable marketing campaigns Create distinct Redirectly links for each marketing channel. Measure which campaigns drive the most installs and re-engagements. Track attribution data in your Redirectly dashboard. ### Referral programs Use deep link parameters to track referrer IDs. When a user installs via a referral link, your app receives the referrer information and can update their profile accordingly. ### Email verification flows Send email verification links that automatically open your app and confirm the email. Deep link parameters carry the verification token, no manual copy-paste needed. ### Password reset & magic links Create secure reset links that deep link directly to the reset flow. Redirectly handles deferred linking so users see the reset screen even on first install. ## Next steps Ready to add deferred deep linking to your Expo app? - [Flutter Deferred Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) - [All Redirectly Integrations](https://redirectly.app/integrations.md) - [Deep Linking for Onboarding](https://redirectly.app/blog/app-onboarding-deferred-deep-linking.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Supabase Deep Linking with Redirectly > Integrate Redirectly with Supabase for email verification and magic link flows in Flutter. Handle auth deep links and verify users on first app launch. - Canonical: https://redirectly.app/integrations/supabase - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete guide to implementing email verification and magic link flows in Flutter. Learn how to handle authentication deep links and verify users when they launch your app. ## Supabase + Redirectly for auth flows Supabase provides excellent authentication infrastructure, and Redirectly enables deferred deep linking for authentication flows. When users click email verification or magic link emails, Redirectly ensures they reach the right screen even if your app isn't installed yet. ### The challenge - User receives email verification link from Supabase - If they haven't installed your app yet, the link doesn't work - Manual copy-paste of tokens is frustrating and error-prone - Users abandon signup or email verification flows ### The solution 1. Generate Redirectly links in your Supabase auth hooks 2. Include verification token in deep link parameters 3. User clicks link, installs app if needed, and is auto-verified 4. Seamless onboarding experience ## Email verification flow implementation Set up email verification using Supabase and Redirectly together. This example shows how to generate verification links that work before app installation. ```dart // lib/services/supabase_auth_service.dart import 'package:supabase_flutter/supabase_flutter.dart' import 'package:redirectly/redirectly.dart' import 'package:http/http.dart' as http class SupabaseAuthService { final supabase = Supabase.instance.client Future signUpWithEmail( String email, String password, ) async { try { // Sign up with Supabase await supabase.auth.signUp( email: email, password: password, ) // Generate Redirectly deep link for verification final deepLink = await _generateVerificationLink(email) // Send custom email with Redirectly link // (Or configure Supabase auth emails to use your domain) await _sendVerificationEmail(email, deepLink) } catch (e) { print('Sign up error: $e') } } Future _generateVerificationLink(String email) async { final session = supabase.auth.currentSession final token = session?.user.id ?? '' // Create Redirectly deep link with verification token final redirectlyUrl = Uri.https( 'api.redirectly.app', '/link/create', { 'apiKey': 'your_api_key', 'path': 'verify-email', 'query': 'token=$token&email=$email', 'subdomain': 'myapp', }, ) final response = await http.post( redirectlyUrl, headers: {'Content-Type': 'application/json'}, ) if (response.statusCode == 200) { final json = jsonDecode(response.body) return json['link'] as String } throw Exception('Failed to create link') } Future _sendVerificationEmail( String email, String deepLink, ) async { // Use your email service to send verification email // Include the Redirectly link print('Send verification email to $email with link: $deepLink') } } ``` The key insight: include the verification token as a query parameter in the Redirectly deep link. When your app launches, retrieve this token and confirm it with Supabase. ## Verify emails when app launches In your app's main widget, listen for deep link events and handle email verification automatically. ```dart // lib/main.dart import 'package:flutter/material.dart' import 'package:redirectly/redirectly.dart' import 'services/supabase_auth_service.dart' void main() async { WidgetsFlutterBinding.ensureInitialized() await Redirectly.initialize( apiKey: 'your_api_key', ) runApp(const MyApp()) } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key) @override State createState() => _MyAppState() } class _MyAppState extends State { final authService = SupabaseAuthService() @override void initState() { super.initState() _setupDeepLinkListener() } void _setupDeepLinkListener() { // Listen to deep links from Redirectly Redirectly.onDeepLink.listen((link) { print('Deep link received: ${link.path}') print('Query params: ${link.params}') // Handle email verification flow if (link.path == 'verify-email') { final token = link.params['token'] as String? final email = link.params['email'] as String? if (token != null && email != null) { _handleEmailVerification(token, email) } } }) } Future _handleEmailVerification( String token, String email, ) async { try { // Verify with Supabase using the token final response = await authService .supabase .auth .verifyOTP( email: email, type: OtpType.email, token: token, ) if (response.session != null) { // User is verified, navigate to main app Navigator.of(context).pushReplacementNamed('/home') ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Email verified!')), ) } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Verification failed: $e')), ) } } @override Widget build(BuildContext context) { return MaterialApp( title: 'MyApp', home: const AuthScreen(), ) } } ``` ## Passwordless auth with magic links Supabase also supports passwordless sign-in with magic links. Use the same Redirectly integration for an even smoother experience. ```dart // Magic link sign-in with Redirectly deep linking Future signInWithMagicLink(String email) async { try { // Generate Redirectly deep link for magic link flow final deepLink = await _generateMagicLinkDeepLink(email) // Sign in with Supabase magic link await supabase.auth.signInWithOtp( email: email, emailRedirectTo: deepLink, // Use Redirectly link ) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Check your email for the Redirectly deep link', ), ), ) } catch (e) { print('Magic link error: $e') } } // Redirectly link for magic link flow Future _generateMagicLinkDeepLink(String email) async { final response = await http.post( Uri.https('api.redirectly.app', '/link/create'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'apiKey': 'your_api_key', 'path': 'magic-link-signin', 'query': 'email=$email', 'subdomain': 'myapp', }), ) if (response.statusCode == 200) { return jsonDecode(response.body)['link'] } throw Exception('Failed to create magic link') } ``` ## Key considerations for secure auth flows ### Token expiration Set reasonable expiration times on verification tokens. Supabase automatically expires OTP tokens after 15 minutes by default. Consider the time it takes for app installation. ### Secure link generation Always generate Redirectly links server-side using your API key. Never expose tokens in client-side code. Use HTTPS for all link generation and token transmission. ### Redirect URL validation Validate that deep link parameters match your expected schema before processing authentication. Prevent malicious actors from injecting false verification data. ### Fallback handling Provide a fallback mechanism for users who don't have your app installed. Include a web-based verification page as a backup option. ## Next steps Combine Supabase auth with Redirectly deferred deep linking: - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [Email Verification Deep Linking](https://redirectly.app/blog/email-verification-deep-linking.md) - [All Integrations](https://redirectly.app/integrations.md) - [React Native Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # FlutterFlow Deep Linking with Redirectly > Add deferred deep linking to FlutterFlow apps with Redirectly. No custom code required—just configure custom actions and handle navigation visually. - Canonical: https://redirectly.app/integrations/flutterflow - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Complete guide to implementing deferred deep linking in FlutterFlow. No custom code required—just custom actions and navigation configuration. ## Deep linking for FlutterFlow apps FlutterFlow empowers developers to build beautiful Flutter apps without writing code. Redirectly extends that no-code philosophy to deferred deep linking and install attribution. Generate Redirectly links directly from FlutterFlow custom actions, and handle deep link events in navigation flows—all without touching native code. ### No-code advantages - **Pure visual building:** All deep linking configuration in FlutterFlow UI - **Custom actions:** Call Redirectly API to create and manage links - **Navigation automation:** Route users based on deep link parameters - **Real-time analytics:** Track attribution in Redirectly dashboard ### Typical flow 1. Create custom action in FlutterFlow to generate Redirectly link 2. Share the link through social, email, or messaging 3. User clicks link and installs your app if needed 4. FlutterFlow navigation handles the deep link automatically ## Create custom actions for Redirectly In FlutterFlow, custom actions let you call external APIs. Create a custom action to generate Redirectly links without leaving your app builder. ### Step 1: Create API call custom action In FlutterFlow, go to Custom Functions → Create new custom action. Set up an HTTP POST request to Redirectly API: ```http POST /link/create Headers: Content-Type: application/json Authorization: Bearer YOUR_API_KEY Body: { "path": "promo/summer2024", "query": "campaign=social&source=instagram", "subdomain": "myapp", "description": "Summer promo campaign link" } Response: { "link": "https://myapp.redirectly.app/promo/summer2024?campaign=social&source=instagram", "id": "link_abc123", "created_at": "2024-03-07T10:30:00Z" } ``` ### Step 2: Store the generated link After calling the Redirectly API, store the returned link in a local variable or global app state so you can share it: ```dart // In your custom action: Future generateRedirectlyLink({ required String path, required String? query, }) async { final response = await makeApiCall( url: 'https://api.redirectly.app/link/create', apiKey: 'your_api_key', body: { 'path': path, 'query': query, 'subdomain': 'myapp', }, ); if (response.statusCode == 200) { final json = jsonDecode(response.body); return json['link'] as String; } throw Exception('Failed to create link'); } // Return the generated link return generatedLink; ``` ## Handle deep links in navigation Configure your FlutterFlow app to listen for deep links and route users to the correct page based on the deep link path. ### Step 1: Enable deep linking in FlutterFlow In Project Settings → App Settings, enable Deep Linking. Add your custom domain: - **iOS:** myapp.redirectly.app - **Android:** myapp.redirectly.app - **Web:** myapp.redirectly.app (optional) ### Step 2: Create page routes for deep links In FlutterFlow page routing, define deep link routes that map paths to pages. For example: ```text Routes: /home → HomePage /promo/:campaignId → PromoScreen - Route parameter: campaignId /product/:productId → ProductDetailScreen - Route parameter: productId /user/:userId → UserProfileScreen - Route parameter: userId // Query parameters (from the 'query' field) ?campaign=social&source=instagram → Available in page context ``` ### Step 3: Access parameters in FlutterFlow pages Once a deep link navigates to a page, access the parameters and use them to customize the page content: **Route parameters:** `widget.parameters['campaignId']` **Query parameters:** `GoRouter.of(context).queryParameters['campaign']` ## Real-world use cases ### Marketing campaigns Generate unique Redirectly links for each campaign, embedded in social media, email, or ads. Track which campaigns drive the most installs. ### Referral programs Create referral links with the referrer ID as a parameter. When the referred user installs and launches, reward the referrer automatically. ### Product sharing Users can generate shareable links for specific products or content. Friends click the link, install your app, and see the exact product they were interested in. ### Email verification Include Redirectly deep links in verification emails. Users click the link, install the app, and are automatically taken to complete their signup. ### Re-engagement campaigns Send deep links to inactive users that open the app directly to relevant new features or content, driving re-engagement without manual navigation. ## Next steps Add deferred deep linking to your FlutterFlow app today: - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) - [Expo Integration](https://redirectly.app/integrations/expo.md) - [All Integrations](https://redirectly.app/integrations.md) - [Referral Programs Guide](https://redirectly.app/blog/referral-program-deep-linking.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Using Redirectly with Firebase > Use Redirectly for deferred deep linking while keeping Firebase for analytics and auth. A clear migration path from sunset Firebase Dynamic Links. - Canonical: https://redirectly.app/integrations/firebase - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Use Redirectly for deferred deep linking while keeping Firebase for auth, analytics, storage, and other services. Learn why Redirectly is a better choice for pure deep linking. ## Firebase Dynamic Links sunset Google is sunsetting Firebase Dynamic Links in favor of Google Play Instant. If you're using FDL for deferred deep linking, install attribution, or link shortening, Redirectly is a direct replacement. You can keep using Firebase for authentication, Firestore, Cloud Storage, and Analytics—just swap the deep linking layer to Redirectly. ### What's changing - **Firebase Dynamic Links (FDL):** Sunsetting, no new features - **Google Play Instant:** Replacement, but different use case (instant apps, not deep linking) - **Impact:** Existing FDL links stop working after sunset date - **Action needed:** Migrate to a new deep linking solution before sunset ### Why choose Redirectly - **Pure focus:** Deep linking is our only job, we do it well - **Transparent pricing:** $0–$80/month, no enterprise sales - **Works with Firebase:** Use both together seamlessly - **Better SDKs:** Pure Dart for Flutter, no native code ## Using Redirectly + Firebase together Redirectly and Firebase serve different purposes. Keep Firebase for what it's great at—and use Redirectly for pure deep linking. ### Architecture diagram 1. **User clicks link** — Redirectly URL (myapp.redirectly.app/...) 2. **Redirectly resolves** — Gets initial deep link parameters, tracks install 3. **App opens with deep link** — App navigates to correct screen 4. **Your app (using Firebase)** — Firebase Auth, Firestore, Analytics ### What to use where #### Redirectly handles - Deep link URLs - Deferred deep linking - Install attribution - Link shortening - Campaign attribution #### Firebase keeps handling - User authentication - Database (Firestore) - Cloud Storage - Analytics events - Cloud Functions ## Migrate from Firebase Dynamic Links If you're currently using Firebase Dynamic Links, migration to Redirectly is straightforward. ```dart // OLD: Firebase Dynamic Links import 'package:firebase_dynamic_links/firebase_dynamic_links.dart' Future createFDL(String deepLink) async { final parameters = DynamicLinkParameters( uriPrefix: 'https://myapp.page.link', link: Uri.parse('https://myapp.com/product/123'), androidParameters: AndroidParameters( packageName: 'com.example.myapp', ), iosParameters: IosParameters( bundleId: 'com.example.myapp', ), ) final link = await FirebaseDynamicLinks.instance.buildLink(parameters) return link.shortUrl.toString() } // NEW: Redirectly import 'package:redirectly/redirectly.dart' Future createRedirectlyLink(String path) async { final link = await Redirectly.createLink( path: path, params: { 'campaign': 'summer_2024', 'source': 'email', }, ) return link } // Usage is simpler! // FDL: Complex setup with Firebase configuration // Redirectly: One API call, transparent pricing, no Firebase dependency ``` The key differences: ### 1. No Firebase dependency required You can use Redirectly independently. Keep Firebase for auth and database, use Redirectly only for deep linking. ### 2. Simpler API FDL requires complex DynamicLinkParameters setup. Redirectly just needs a path and optional parameters. ### 3. Easier link handling Both listen for deep links in the same way, so updating your deep link handler is minimal. ### 4. No vendor lock-in Redirectly uses standard URL schemes. If you ever switch services, you just update your domain. ## Keep using Firebase services Your Firebase setup stays exactly the same. You're just replacing the deep linking layer. ```dart // lib/services/app_service.dart import 'package:firebase_auth/firebase_auth.dart' import 'package:cloud_firestore/cloud_firestore.dart' import 'package:redirectly/redirectly.dart' class AppService { final auth = FirebaseAuth.instance final firestore = FirebaseFirestore.instance Future initialize() async { // Firebase still handles auth if (auth.currentUser != null) { print('User logged in: ${auth.currentUser!.email}') } // Redirectly handles deep linking await Redirectly.initialize(apiKey: 'your_key') // Listen for deep links Redirectly.onDeepLink.listen((link) { if (link.path == 'product/123') { // Still use Firestore to load product final doc = await firestore .collection('products') .doc('123') .get() // Navigate with product data navigateTo('/product', arguments: doc.data()) } }) } Future createProductLink(String productId) async { // Use Redirectly to create shareable link final link = await Redirectly.createLink( path: 'product/$productId', params: {'campaign': 'share'}, ) // Then share or send via Firebase Cloud Messaging await sendShareLink(link) } Future sendShareLink(String link) async { // Still use Firebase Cloud Messaging for push notifications // Or any other Firebase service } } ``` ## FDL vs Redirectly comparison | Feature | FDL | Redirectly | | --- | --- | --- | | Deep linking | Yes | Yes | | Deferred deep linking | Yes | Yes | | Install attribution | Yes | Yes | | Still active/maintained | No | Yes | | Simple API | No | Yes | | Transparent pricing | No | Yes | | Free tier | No | Yes | | Pure Flutter/Dart SDK | No | Yes | | Works without Firebase | No | Yes | | Custom subdomains | Yes | Yes | | Real-time analytics | Yes | Yes | ## Next steps Migrate from Firebase Dynamic Links to Redirectly: - [FDL Alternative Comparison](https://redirectly.app/firebase-dynamic-links-alternative.md) - [Flutter Deep Linking Guide](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) - [All Integrations](https://redirectly.app/integrations.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Segment + Redirectly Attribution > Send Redirectly deep link attribution events to Segment. Track install sources, campaigns, and user attribution across your entire analytics stack with ease. - Canonical: https://redirectly.app/integrations/segment - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Send install attribution and deep link events from Redirectly to Segment. Track which links drive your most valuable installs across your entire analytics stack. ## Attribution data in your analytics stack Redirectly captures rich install attribution data—which link drove the install, which campaign, which source. By forwarding this data to Segment, you centralize attribution alongside all other user behavior, enabling more powerful analytics and downstream integrations. ### Attribution data captured - **Link ID:** Which Redirectly link was clicked - **Campaign:** Campaign from link parameters - **Source:** Where the link was clicked from - **Device:** OS, device type, country - **Custom parameters:** Any data in the deep link ### What Segment enables - **Unified profiles:** Link attribution to user behavior - **Downstream destinations:** Send to 200+ tools - **Audiences:** Build segments based on attribution - **ROI analysis:** Measure campaign profitability - **Real-time dashboards:** Track attribution in real time ## Send attribution to Segment When a user installs your app after clicking a Redirectly link, capture the attribution data and send it to Segment. ```dart // lib/services/attribution_service.dart import 'package:segment_flutter/segment_flutter.dart' import 'package:redirectly/redirectly.dart' class AttributionService { final analytics = Segment() /// Called on app startup to capture install attribution Future captureInstallAttribution() async { try { // Get initial deep link from Redirectly final link = await Redirectly.getInitialLink() if (link != null) { // Track install with attribution data await analytics.track( 'Install Attributed', properties: { 'redirectly_link_id': link.id, 'redirectly_path': link.path, 'campaign': link.params['campaign'], 'source': link.params['source'], 'medium': link.params['medium'], 'content': link.params['content'], 'term': link.params['term'], // Include any custom parameters ...link.params, }, ) // Identify user with attribution data await analytics.identify( userId: getUserId(), traits: { 'attributed_link': link.id, 'install_source': link.params['source'], 'install_campaign': link.params['campaign'], }, ) } } catch (e) { print('Error capturing attribution: $e') } } /// Listen for deep link events during app lifecycle void listenToDeepLinks() { Redirectly.onDeepLink.listen((link) { // Track re-engagement through deep links analytics.track( 'Deep Link Opened', properties: { 'redirectly_link_id': link.id, 'path': link.path, 'campaign': link.params['campaign'], 'source': link.params['source'], ...link.params, }, ) }) } } ``` Call `captureInstallAttribution()` in your main app initialization to track installs. Set up the deep link listener to track re-engagement. ## Advanced attribution tracking Track more granular attribution events and connect them to downstream user behaviors like signup, purchase, and retention. ```dart // Track signup with attribution context Future signupUser(String email, String password) async { try { // Sign up in your backend final user = await _signupBackend(email, password) // Get attribution context from Redirectly final link = await Redirectly.getInitialLink() // Track signup with attribution await analytics.track( 'Signup Completed', userId: user.id, properties: { 'email': email, 'redirectly_link_id': link?.id, 'attributed_source': link?.params['source'], 'attributed_campaign': link?.params['campaign'], }, ) // Identify user await analytics.identify( userId: user.id, traits: { 'email': email, 'install_link': link?.id, 'signup_timestamp': DateTime.now().toIso8601String(), }, ) } catch (e) { print('Signup error: $e') } } // Track purchase with attribution Future recordPurchase(String orderId, double amount) async { final link = await Redirectly.getInitialLink() await analytics.track( 'Purchase Completed', properties: { 'order_id': orderId, 'amount': amount, // Connect to original install attribution 'install_link_id': link?.id, 'install_source': link?.params['source'], 'install_campaign': link?.params['campaign'], }, ) } ``` ## Forward attribution to 200+ tools Once attribution data flows into Segment, you can forward it to any destination. Popular integrations for attribution analysis include: ### Analytics & BI - Mixpanel (funnel attribution) - Amplitude (cohort analysis) - Google Analytics 4 (conversions) - Redshift / BigQuery (data warehouse) - Looker (dashboards) ### Advertising Platforms - Facebook Conversions API - Google Ads conversion tracking - TikTok Events API - Apple Search Ads - Adjust (attribution partner) ### CRM & Email - Hubspot (user properties) - Salesforce (account data) - Mailchimp (audience sync) - Intercom (user profiles) ### Retention & Engagement - OneSignal (push campaigns) - Braze (user journeys) - Apptentive (NPS) - Firebase Analytics ## Next steps Combine Redirectly attribution with Segment for complete analytics: - [Amplitude Attribution](https://redirectly.app/integrations/amplitude.md) - [Mixpanel Integration](https://redirectly.app/integrations/mixpanel.md) - [Flutter Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [All Integrations](https://redirectly.app/integrations.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Amplitude + Redirectly Attribution > Send install attribution and deep link events from Redirectly to Amplitude. Track which campaigns drive your best users and measure ROI by install source. - Canonical: https://redirectly.app/integrations/amplitude - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Send install attribution and deep link events from Redirectly to Amplitude. Track cohorts by install source, measure campaign ROI, and understand which links drive the most valuable users. ## Cohort analysis by attribution source Amplitude excels at behavioral analytics and user segmentation. By integrating Redirectly attribution with Amplitude, you can build cohorts of users acquired from specific campaigns, measure retention by attribution source, and understand the lifetime value of each marketing channel. ### What you measure - **Install source:** Which link drove each install - **Retention by cohort:** D1, D7, D30 retention by campaign - **LTV by channel:** Revenue per user by attribution source - **Feature adoption:** Which users engage with which features - **Conversion funnels:** Sign up → purchase by install source ### Real use cases - Instagram campaign drives 5,000 installs, 8% D7 retention - TikTok campaign drives 2,000 installs, 18% D7 retention - Facebook referral link drives 1,000 installs, 35% D7 retention - Email re-engagement link drives high-value returning users - → Optimize budget toward best-performing channels ## Send attribution events to Amplitude Initialize Amplitude in your app and send attribution events when you capture Redirectly deep links. ```dart // lib/services/amplitude_service.dart import 'package:amplitude_flutter/amplitude.dart' import 'package:redirectly/redirectly.dart' class AmplitudeService { static final _instance = Amplitude.getInstance() static Future initialize(String apiKey) async { await _instance.init(apiKey) await _instance.trackingSessionEvents(true) } /// Track install with attribution data from Redirectly static Future trackInstallAttribution() async { final link = await Redirectly.getInitialLink() if (link != null) { // Build event properties from Redirectly deep link final eventProperties = { 'redirectly_link_id': link.id, 'redirectly_path': link.path, 'campaign': link.params['campaign'], 'source': link.params['source'], 'medium': link.params['medium'], 'content': link.params['content'], } // Track install attributed event _instance.logEvent( 'Install Attributed', eventProperties: eventProperties, ) // Set user properties for cohort analysis _instance.setUserProperties({ 'install_link': link.id, 'install_campaign': link.params['campaign'], 'install_source': link.params['source'], 'install_timestamp': DateTime.now().toIso8601String(), }) print('Tracked install attribution: ${link.id}') } } /// Listen for deep link events during session static void listenToDeepLinks() { Redirectly.onDeepLink.listen((link) { _instance.logEvent( 'Deep Link Opened', eventProperties: { 'path': link.path, 'campaign': link.params['campaign'], 'source': link.params['source'], 'redirectly_link_id': link.id, }, ) }) } } ``` Call these methods in your app's main initialization to start tracking attribution: ```dart // main.dart import 'package:flutter/material.dart' import 'services/amplitude_service.dart' void main() async { WidgetsFlutterBinding.ensureInitialized() // Initialize Redirectly await Redirectly.initialize(apiKey: 'your_redirectly_key') // Initialize Amplitude await AmplitudeService.initialize('your_amplitude_key') // Track initial attribution await AmplitudeService.trackInstallAttribution() // Listen for future deep link events AmplitudeService.listenToDeepLinks() runApp(const MyApp()) } ``` ## Build cohorts with user properties Set user properties in Amplitude so you can segment users by their install attribution source. This powers cohort analysis and retention tracking. ```dart // Set detailed user properties for cohort building Future setAttributionProperties() async { final link = await Redirectly.getInitialLink() if (link != null) { amplitude.setUserProperties({ // Campaign tracking 'install_campaign': link.params['campaign'], 'install_source': link.params['source'], 'install_medium': link.params['medium'], // Time-based properties for lifecycle analysis 'install_date': DateTime.now().toIso8601String(), 'install_week': DateTime.now().week.toString(), 'install_month': DateTime.now().month.toString(), // Custom parameters from your links 'referrer_id': link.params['referrer'], 'promo_code': link.params['promo'], 'cohort': link.params['cohort'], // Analytics flags 'is_organic': link.params['source'] == 'organic', 'is_paid': link.params['source'] == 'paid', 'is_referral': link.params['source'] == 'referral', }) } } // Now you can segment users: // - All Instagram installs: install_source = "instagram" // - Summer 2024 campaign: install_campaign = "summer_2024" // - High-value cohort: referrer_id exists // - Organic traffic: is_organic = true ``` ## Measure retention by attribution source In Amplitude, use Retention Chart and Cohort Analysis to measure D1, D7, D30 retention for each traffic source. ### Retention chart setup In Amplitude, create a Retention chart with: - **Cohort:** Users who triggered "Install Attributed" event - **Segmented by:** install_source user property - **Retention event:** Any event (or specific events like "Session Start") - **Time periods:** D1, D7, D14, D30 ### Expected insights You'll see retention curves like: - Instagram: 8% D1 → 5% D7 → 2% D30 - TikTok: 18% D1 → 12% D7 → 6% D30 - Facebook referral: 35% D1 → 28% D7 → 18% D30 - Email re-engagement: 45% D1 → 38% D7 → 25% D30 ### Act on the data Use these insights to optimize: reallocate budget toward highest-retention sources, improve onboarding for low-retention cohorts, or refocus campaigns on best-performing channels. ## Attribution-aware funnel analysis Track user conversion funnels and measure dropoff by installation source. ```dart // Track funnel events with attribution context class FunnelTracking { static Future trackSignup(String email) async { final link = await Redirectly.getInitialLink() amplitude.logEvent( 'User Signup', eventProperties: { 'email': email, 'install_source': link?.params['source'], 'install_campaign': link?.params['campaign'], }, ) } static Future trackPurchase( String orderId, double amount, ) async { final link = await Redirectly.getInitialLink() amplitude.logEvent( 'Purchase Completed', eventProperties: { 'order_id': orderId, 'amount': amount, 'install_source': link?.params['source'], 'install_campaign': link?.params['campaign'], }, ) } static Future trackFeatureUsage(String featureName) async { final link = await Redirectly.getInitialLink() amplitude.logEvent( 'Feature Used', eventProperties: { 'feature': featureName, 'install_source': link?.params['source'], }, ) } } // Amplitude funnel: // Install → (50% from Instagram, 80% from TikTok) // Signup → (40% from Instagram, 70% from TikTok) // Purchase → (15% from Instagram, 45% from TikTok) // → Clearly TikTok users are more valuable ``` ## Next steps Understand your users better with Amplitude + Redirectly attribution: - [Segment Integration](https://redirectly.app/integrations/segment.md) - [Mixpanel Attribution](https://redirectly.app/integrations/mixpanel.md) - [Flutter Deferred Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [All Integrations](https://redirectly.app/integrations.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Mixpanel + Redirectly Attribution > Track install attribution and deep link events in Mixpanel with Redirectly. Measure which campaigns drive engaged users and analyze funnels by source. - Canonical: https://redirectly.app/integrations/mixpanel - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Track install attribution and deep link events in Mixpanel. Measure campaign ROI, analyze user engagement by source, and optimize based on behavioral insights. ## Behavioral analytics with attribution Mixpanel excels at behavioral product analytics. By combining Redirectly attribution data with Mixpanel event tracking, you understand not just which campaigns drive installs, but which drive engaged, high-value users. Track user paths, measure feature adoption by campaign, and optimize marketing toward behavioral outcomes. **What Mixpanel measures:** - **User funnels:** Drop-off rates by campaign - **Retention:** Which cohorts stay engaged - **User paths:** Common feature usage sequences - **Segmentation:** Cohort analysis by attribution - **Trend analysis:** How campaigns perform over time **Real-world insights:** - Organic traffic → High retention, low purchase conversion - Paid ads → High retention, high purchase rate - Email re-engagement → Highest lifetime value - Instagram influencer → Viral potential, high acquisition - → Optimize marketing mix based on behavior, not just installs ## Send attribution events to Mixpanel Initialize Mixpanel and send attribution data when your app detects a Redirectly deep link on startup. ```dart // lib/services/mixpanel_service.dart import 'package:mixpanel_flutter/mixpanel_flutter.dart' import 'package:redirectly/redirectly.dart' class MixpanelService { static final Mixpanel _instance = Mixpanel() static Future initialize(String token) async { await _instance.init(token) await _instance.setLoggingEnabled(false) } /// Track install with attribution from Redirectly static Future trackInstallAttribution() async { try { final link = await Redirectly.getInitialLink() if (link != null) { // Build properties from Redirectly data final properties = { 'redirectly_link_id': link.id, 'redirectly_path': link.path, 'campaign': link.params['campaign'], 'source': link.params['source'], 'medium': link.params['medium'], 'content': link.params['content'], 'term': link.params['term'], } // Track the install event await _instance.track('Install Attributed', properties) // Set user properties for segmentation await _instance.setUserProperties({ 'install_link': link.id, 'install_source': link.params['source'], 'install_campaign': link.params['campaign'], 'install_time': DateTime.now().toIso8601String(), }) print('Tracked install: ${link.id}') } } catch (e) { print('Error tracking install: $e') } } /// Listen for deep links during session static void listenToDeepLinks() { Redirectly.onDeepLink.listen((link) { _instance.track( 'Deep Link Opened', properties: { 'path': link.path, 'redirectly_link_id': link.id, 'campaign': link.params['campaign'], 'source': link.params['source'], }, ) }) } /// Track custom events with attribution context static Future trackEvent( String eventName, Map? properties, ) async { final link = await Redirectly.getInitialLink() final eventProperties = { ...?properties, if (link != null) ...{ 'install_source': link.params['source'], 'install_campaign': link.params['campaign'], }, } await _instance.track(eventName, eventProperties) } } ``` Initialize this in your app's main entry point: ```dart // main.dart void main() async { WidgetsFlutterBinding.ensureInitialized() await Redirectly.initialize(apiKey: 'your_redirectly_key') await MixpanelService.initialize('your_mixpanel_token') // Track initial attribution await MixpanelService.trackInstallAttribution() // Listen for future deep links MixpanelService.listenToDeepLinks() runApp(const MyApp()) } ``` ## Track user behavior with attribution context Log every important user action with campaign attribution. This reveals which channels drive engaged users. ```dart // lib/screens/auth_screen.dart class AuthScreen extends StatelessWidget { Future handleSignup(String email, String password) async { try { // Sign up the user final user = await _auth.signup(email, password) // Track signup with attribution await MixpanelService.trackEvent( 'Signup Completed', {'email': email, 'user_id': user.id}, ) Navigator.pushReplacementNamed(context, '/home') } catch (e) { print('Signup error: $e') } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Sign Up')), body: Column( children: [ TextField( onChanged: (value) { // Track field interaction MixpanelService.trackEvent('Signup Form Focused') }, ), ElevatedButton( onPressed: () => handleSignup(email, password), child: const Text('Sign Up'), ), ], ), ) } } // lib/screens/home_screen.dart class HomeScreen extends StatefulWidget { @override void initState() { super.initState() // Track screen view MixpanelService.trackEvent('Home Screen Viewed') } void handlePurchase(String productId, double amount) async { await _stripe.charge(amount) // Track purchase with attribution await MixpanelService.trackEvent( 'Purchase Completed', { 'product_id': productId, 'amount': amount, }, ) } void shareWithFriend(String friendEmail) async { // Generate Redirectly referral link final referralLink = await Redirectly.createLink( path: 'referral', params: { 'referrer_id': currentUser.id, 'referrer_email': currentUser.email, }, ) // Track referral event await MixpanelService.trackEvent( 'Referral Shared', { 'friend_email': friendEmail, 'referral_link': referralLink, }, ) // Share via email/SMS await Share.share('Join me: $referralLink') } } ``` ## Analyze funnels by campaign source In Mixpanel, create funnels that show how users from each campaign progress through your app. ### Setup funnel in Mixpanel Create a funnel with these steps: 1. Install Attributed 2. Signup Completed 3. First Purchase Completed Segment by: install_source ### Typical funnel results You might see something like: - **Instagram:** 1000 installs → 200 signups (20%) → 30 purchases (3%) - **Email:** 100 installs → 85 signups (85%) → 25 purchases (25%) - **Organic:** 500 installs → 75 signups (15%) → 15 purchases (3%) ### Use insights to optimize If email drives 5x higher conversion despite lower volume, shift budget toward email. If Instagram has low conversion but high viral potential, focus on amplifying top creators. ## Discover user paths by attribution Use Mixpanel's User Path analysis to see how users from each campaign navigate your app. ```dart // Example: Set detailed user properties for path analysis Future setDetailedUserProperties() async { final link = await Redirectly.getInitialLink() if (link != null) { await mixpanel.setUserProperties({ // Campaign attribution 'install_campaign': link.params['campaign'], 'install_source': link.params['source'], // Device info 'device_type': deviceType, 'os_version': osVersion, // Behavioral properties 'is_power_user': false, // Update as user engages 'feature_interest': [], // Build as user explores 'price_sensitivity': 'unknown', // Learn from behavior }) } } // Then track events: // Home Screen Viewed → Browse Products → Add to Cart → Purchase // Mixpanel will show: // "Instagram users spend 40s on Product pages" // "Email users buy within 2 hours of install" // "Organic users have 5x higher churn" ``` ## Next steps - [Amplitude Integration](https://redirectly.app/integrations/amplitude.md) - [Segment Integration](https://redirectly.app/integrations/segment.md) - [Flutter Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [All Integrations](https://redirectly.app/integrations.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # OneSignal + Redirectly Deep Linking > Use Redirectly deep links in OneSignal push notifications to route users straight to relevant app screens and measure engagement with attribution tracking. - Canonical: https://redirectly.app/integrations/onesignal - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Send deep-linked push notifications using OneSignal and Redirectly. Route users directly to relevant app screens and measure engagement with attribution tracking. ## Push notifications + deep linking Deep-linked push notifications are significantly more effective than generic notifications. Instead of opening to the home screen, users land directly on the content you want them to see. Combine OneSignal's powerful push targeting with Redirectly's deep linking for engagement that converts. **Without deep linking:** - Push notification: "New product launched!" - User taps → Opens home screen - User searches for product manually - Many users give up before finding it - Low conversion rate **With Redirectly deep linking:** - Push notification: "New product launched!" - User taps → Opens product detail page - Immediate call-to-action visible - Zero friction to purchase - 3-5x higher conversion rate ## Create deep-linked push notifications In OneSignal, you can set launch URLs for push campaigns. Instead of using standard URLs, use Redirectly deep links to ensure users go to the right app screen. ### Step 1: Generate Redirectly deep links Create unique Redirectly links for each push campaign: ```text // Create links for your OneSignal campaigns Campaign: "New Summer Collection" Link: https://myapp.redirectly.app/product/summer-2024 Query: campaign=summer&source=onesignal&medium=push Campaign: "Flash Sale" Link: https://myapp.redirectly.app/sale Query: campaign=flash_sale&source=onesignal&medium=push&discount_code=FLASH2024 Campaign: "Abandoned Cart Reminder" Link: https://myapp.redirectly.app/cart Query: campaign=abandoned_cart&source=onesignal&medium=push Campaign: "Win 50% Off" Link: https://myapp.redirectly.app/promotion/vip Query: campaign=loyalty_50off&source=onesignal&medium=push ``` ### Step 2: Configure OneSignal campaign In OneSignal, paste the Redirectly link as the launch URL: ```text OneSignal Campaign Settings: Campaign Name: New Summer Collection Message: Check out our new summer products! Launch URL: https://myapp.redirectly.app/product/summer-2024?campaign=summer&source=onesignal&medium=push ``` OneSignal will handle routing the deep link when users tap the notification. ## Handle deep links in your app Your app must be configured to listen for deep links and route users appropriately when they tap a OneSignal push notification. ```dart // lib/services/notification_service.dart import 'package:onesignal_flutter/onesignal_flutter.dart' import 'package:redirectly/redirectly.dart' class NotificationService { static Future initialize( String oneSignalAppId, String redirectlyApiKey, ) async { // Initialize OneSignal OneSignal.initialize(oneSignalAppId) // Initialize Redirectly await Redirectly.initialize(apiKey: redirectlyApiKey) // Handle notification opened (user taps push) OneSignal.Notifications.addForegroundWillDisplayListener( (OSNotificationReceivedEvent event) { // Don't show notification if it has a launch URL // (handled by deep linking) event.notification.display = false }, ) OneSignal.Notifications.addClickListener((event) { final launchUrl = event.notification.launchUrl if (launchUrl != null) { // OneSignal handles the deep link navigation print('Notification opened with launch URL: $launchUrl') } }) // Handle cold start deep links (app not running) Redirectly.onDeepLink.listen((link) { print('Deep link from notification: ${link.path}') _handleDeepLink(link) }) } static Future _handleDeepLink( RedirectlyLink link, ) async { // Track which campaign drove the tap if (link.params['campaign'] != null) { print('User tapped ${link.params['campaign']} campaign') } // Navigate based on the deep link if (link.path == 'product/summer-2024') { navigatorKey.currentState?.pushNamed('/product', arguments: { 'productId': 'summer-2024', 'campaign': link.params['campaign'], }) } else if (link.path == 'sale') { navigatorKey.currentState?.pushNamed('/sale') } else if (link.path == 'cart') { navigatorKey.currentState?.pushNamed('/cart') } } } ``` ## Campaign ideas with deep linking ### Product launches & updates Send push notifications with deep links directly to the new feature or product page. Users see it immediately without searching. ### Flash sales & promotions Use Redirectly query parameters to pass discount codes or campaign IDs. When users tap the push, the discount is already available on the sale page. ### Abandoned cart recovery Deep link directly to the user's cart. Include a discount code as a parameter to incentivize completion. ### Win-back campaigns Target inactive users with deep links to new content or exclusive offers. They land exactly where you want them. ### Referral rewards Send push notifications to referrers when their friends sign up. Deep link to a rewards page with their bonus offer. ### Event-triggered notifications Use OneSignal's automation to send triggered pushes when users match certain criteria. Deep link to the most relevant content for that user. ## Measure push campaign effectiveness Track which push campaigns drive the most valuable actions. Use deep link parameters to attribute user behavior back to specific notifications. ```dart // Track push-driven actions Future trackPurchaseFromPush( String orderId, double amount, ) async { final link = await Redirectly.getInitialLink() // Log event with campaign attribution analytics.track( 'Purchase from Push', properties: { 'order_id': orderId, 'amount': amount, 'push_campaign': link?.params['campaign'], 'push_source': 'onesignal', }, ) // Update user properties if (link != null) { analytics.setUserProperties({ 'push_campaign_convert': link.params['campaign'], 'last_push_interaction': 'purchase', }) } } // Segment users by push engagement // OneSignal segment: "Tapped summer campaign" // → Users where push_campaign = "summer" and last_push_interaction = "purchase" // → Measure LTV, repeat purchase rate, etc. ``` ## Next steps - [Segment Analytics Integration](https://redirectly.app/integrations/segment.md) - [Amplitude Attribution](https://redirectly.app/integrations/amplitude.md) - [Flutter Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [All Integrations](https://redirectly.app/integrations.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Link Generator > Generate deep links for iOS, Android, and web with campaign parameters. Build and copy custom mobile deep links instantly — free, no signup. - Canonical: https://redirectly.app/tools/deep-link-generator - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Build mobile deep links with custom parameters, fallback URLs, and campaign tracking. Generate, preview, and copy deep links instantly. ## Build your deep link This is an interactive tool: enter a destination URL (the app screen or path to open), choose a target platform (iOS, Android, or both), and add App Store, Play Store, and web fallback URLs, campaign parameters (UTM source, medium, and campaign), and custom key/value parameters. The tool generates the deep link live so you can copy it to your clipboard. Use the interactive generator at https://redirectly.app/tools/deep-link-generator ## What is a deep link? A deep link is a URL that opens a specific location within a mobile app instead of just launching the app at its home screen. When a user taps a deep link on their phone and your app is installed, the app opens directly to the intended content or screen, creating a seamless experience. ### Without deep links (regular web link) 1. User clicks link on mobile 2. Website opens in mobile browser 3. User manually downloads app (if not installed) 4. User searches for content again in app ### With deep links 1. User clicks deep link on mobile 2. App opens if installed (or user is directed to app store) 3. App opens directly to the specific content 4. Seamless, frictionless user experience Deep links are essential for marketing, social sharing, email campaigns, and any situation where you want users to land on specific app content. They significantly improve user experience and conversion rates by reducing friction in the user journey. ## Deep link URL structure Deep links follow a consistent URL format that allows apps to route users to the correct screen. Here's how they're structured: ### Basic Structure ``` scheme://host/path?parameter=value&other=value ``` #### Example: E-commerce Product Page ``` myapp://products/12345?utm_source=email&utm_campaign=sale ``` #### Example: Social Media Post ``` myapp://feed/post/abc123?utm_source=instagram ``` #### Example: With Multiple Parameters ``` myapp://checkout?item_id=456&referral=friend123&utm_source=referral ``` ### Scheme Custom protocol like `myapp://`. Must be configured in app settings (iOS URL Schemes, Android Intent Filters). ### Host Identifies the app or section. Example: `products`, `checkout`, `profile`. ### Path Specifies the resource or content ID. Example: `/12345` for product ID, `/user/john` for username. ### Query Parameters Additional data and campaign tracking. Example: `utm_source=email&utm_campaign=summer_sale`. ## Deferred vs. standard deep links Understanding the difference between standard and deferred deep links helps you choose the right approach for your use case: | Aspect | Standard Deep Link | Deferred Deep Link | | --- | --- | --- | | App Required | Must be installed | Works even if not installed | | Install Flow | Direct navigation to app screen | Redirect to store → open to screen after install | | User Context | Preserved if app is open | Preserved across install | | Best For | Social sharing, existing users | Email, uninstalled users, acquisitions | | Complexity | Simple to implement | Requires backend infrastructure | ### Use Standard Deep Links When - Most of your users have the app installed - You're sharing content within the app itself - You need quick implementation without backend - You're not worried about new user onboarding ### Use Deferred Deep Links When - You're acquiring new users via email or ads - Users may not have your app installed yet - You need to preserve context across install - You're sending verification or password reset links ## Ready to use deep links at scale? Save your deep link configurations, track performance, and manage deferred links with Redirectly: - [Read: Complete Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [Learn: Deferred Deep Linking](https://redirectly.app/deferred-deep-linking.md) - [Test: Deep Link Tester Tool](https://redirectly.app/tools/deep-link-tester.md) ## Frequently asked questions ### What is a deep link? A deep link is a URL that directs users to a specific location within a mobile app. Unlike regular links that go to websites, deep links open content or screens directly inside an installed app, creating a seamless user experience. ### What is the difference between standard and deferred deep links? Standard deep links work only if the app is already installed. Deferred deep links store the intended destination and redirect users to install the app first, then open to the correct screen after installation is complete. ### Can I add UTM parameters to deep links? Yes. This generator allows you to add utm_source, utm_medium, utm_campaign, utm_term, and utm_content parameters to track campaign performance for deep linked traffic. ### What are fallback URLs? Fallback URLs direct users to alternative destinations when they cannot open the deep link. For example, if a user doesn't have your app installed, they can be sent to the App Store, Play Store, or your website. ### How do I test if a deep link works? Use the Redirectly Deep Link Tester tool to validate your deep links. It will show you the parsed URL components, detected platform, and simulate what happens when the link is opened on different devices. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Link Tester > Test and debug deep links for iOS, Android, and web. Validate deep link URLs and preview how they behave on different platforms — free. - Canonical: https://redirectly.app/tools/deep-link-tester - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Validate and debug deep links for iOS, Android, and web. Parse deep link URLs, detect platforms, and identify issues before launching campaigns. ## Test your deep link This is an interactive tool: paste a deep link URL (iOS Universal Links, Android App Links, custom schemes like `myapp://`, or regular web URLs) at [https://redirectly.app/tools/deep-link-tester](https://redirectly.app/tools/deep-link-tester) and click "Test Link". The tool parses the URL into its components (scheme, host, path, query, fragment), detects the target platform (iOS / Android / Web), extracts UTM parameters, flags issues (spaces, http instead of https, custom schemes that won't work as Universal/App Links, missing campaign parameters), and shows the platform-specific redirect flow: **On iOS:** 1. Deep link tapped on iOS device 2. iOS checks if domain has AASA file (Apple App Site Association) 3. If associated → App opens directly to deep linked content 4. If not associated → Safari opens with fallback URL 5. UTM parameters preserved throughout the flow **On Android:** 1. Deep link tapped on Android device 2. Android checks if domain has assetlinks.json file 3. If verified → App opens directly to deep linked content 4. If not verified → Chrome/default browser opens fallback URL 5. UTM parameters preserved throughout the flow **On Web:** 1. Link tapped on web browser or device without app 2. Default web browser opens the URL 3. Content loads in browser with full UTM tracking 4. Analytics platform captures campaign data 5. User can manually install app from landing page ## Why test deep links? Deep link testing is essential to ensure your app links work correctly across iOS, Android, and web. Broken deep links frustrate users and damage your campaign performance. **Consequences of broken deep links:** - Users land on wrong screen or home page - Campaign tracking data is lost - Lower conversion rates and engagement - Poor user experience and app uninstalls **Benefits of tested deep links:** - Catch issues before launching campaigns - Ensure campaign tracking works correctly - Improve conversion rates and user experience - Verify platform-specific behavior **When to test deep links:** - Before launching any campaign with deep links - When creating new deep link schemes or routes - After updating your app's deep link configuration - When adding new campaign parameters or UTM values ## Common deep link issues ### Missing AASA File (iOS) The Apple App Site Association (AASA) file is required for Universal Links to work on iOS. Without it, links open in Safari instead of your app. **Solution:** Host an AASA file at `https://yourdomain.com/.well-known/apple-app-site-association`. Use the [AASA Validator tool](https://redirectly.app/aasa-validator.md). ### Missing assetlinks.json (Android) Android requires an assetlinks.json file to verify the association between your app and domain. Without it, App Links won't work. **Solution:** Host an assetlinks.json file at `https://yourdomain.com/.well-known/assetlinks.json`. Use the [assetlinks.json Validator tool](https://redirectly.app/assetlinks-validator.md). ### Using HTTP Instead of HTTPS iOS Universal Links and Android App Links require HTTPS. HTTP links won't work for verified deep linking and will fall back to browser opening. **Solution:** Always use https:// for deep links, not http:// ### Incorrect Scheme Name Custom scheme deep links (myapp://) must match exactly the scheme registered in your app's configuration. Typos or mismatches cause the app not to launch. **Examples:** - Your app registers: `myapp` - Deep link uses: `myapp://` — Correct - Deep link uses: `my-app://` — Won't work ### Spaces in URLs URLs with spaces are invalid and won't be recognized by browsers or apps. All spaces must be encoded as %20. **Example:** - Wrong: `myapp://products/summer sale` - Correct: `myapp://products/summer%20sale` ## How to debug deep links Follow these steps to identify and fix deep link issues: ### Step 1: Test with the Deep Link Tester Paste your deep link URL into this tool to see if it parses correctly and check for obvious issues. Look for: - Invalid URL format - Missing or incorrect components - UTM parameter capture issues ### Step 2: Verify Platform Configuration For iOS: Check if your AASA file is properly configured and accessible — [validate your AASA file](https://redirectly.app/aasa-validator.md). For Android: Check if your assetlinks.json file is properly configured and accessible — [validate your assetlinks.json](https://redirectly.app/assetlinks-validator.md). ### Step 3: Test on Real Devices The best way to test deep links is on actual iOS and Android devices. Install your app and try tapping the deep link from various sources (email, SMS, browser, etc.). **Note:** Some deep link functionality (like Universal Links on iOS) only works when tapping links from external apps or browsers, not when testing in Xcode. ### Step 4: Check App Logs Review your app's logs to see if the deep link is being received by your app and parsed correctly. Add logging to your deep link handling code: ```javascript // In your app's deep link handler console.log('Received deep link:', url); console.log('Parsed parameters:', params); console.log('UTM source:', params.utm_source); ``` ### Step 5: Verify Analytics Integration Make sure your analytics SDK is properly initialized and configured to capture deep link parameters. Test a deep link and check if the UTM data appears in your analytics dashboard. ## Related tools and guides - [Validate: AASA File Validator](https://redirectly.app/aasa-validator.md) - [Validate: assetlinks.json Validator](https://redirectly.app/assetlinks-validator.md) - [Build: Deep Link Generator](https://redirectly.app/tools/deep-link-generator.md) ## Frequently asked questions ### How do I test a deep link? Paste your deep link URL into this tester tool, click "Test Link", and the tool will analyze it. It will show you the parsed URL components, detected platform, and any potential issues. ### What are common deep link issues? Common issues include: using http:// instead of https://, missing AASA file for iOS, missing assetlinks.json for Android, incorrect scheme names, spaces in URLs, and missing app configuration. ### What is an AASA file? The Apple App Site Association (AASA) file is a JSON configuration file that you host on your server at /.well-known/apple-app-site-association. It tells iOS that your app is associated with your domain, enabling Universal Links. ### What is assetlinks.json? The assetlinks.json file is an Android configuration file that you host on your server at /.well-known/assetlinks.json. It tells Android that your app is associated with your domain, enabling App Links. ### Why isn't my deep link opening the app? This can happen for several reasons: the app is not installed, the deep link scheme is not registered in the app manifest, the domain is not verified for Universal Links/App Links, or there's a URL encoding issue. Use this tester to identify the problem. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # UTM Builder for Mobile Apps > Build UTM parameters for mobile app deep links and campaigns. Track campaign performance across iOS, Android, and web — free, no signup. - Canonical: https://redirectly.app/tools/utm-builder - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Create trackable campaign links with UTM parameters. Build URLs for iOS, Android, and web campaigns. See real-time platform behavior and previews. ## Build your campaign link This is an interactive tool: enter a base URL (or deep link like `myapp://products`), the required UTM parameters (utm_source, utm_medium, utm_campaign), and optionally utm_term and utm_content at [https://redirectly.app/tools/utm-builder](https://redirectly.app/tools/utm-builder). The tool generates a properly URL-encoded campaign link you can copy to your clipboard. ## See how your campaign link behaves Your campaign link behaves differently depending on the platform: **iOS Behavior** — On iOS, if your app supports Universal Links: 1. iOS checks if the domain is associated with your app (via AASA file) 2. If associated, the app opens directly with UTM params intact 3. If not associated, link opens in Safari mobile browser 4. UTM data is captured in your analytics dashboard **Android Behavior** — On Android, if your app supports App Links: 1. Android verifies the domain association (via assetlinks.json) 2. If verified, the app opens directly with UTM params intact 3. If not verified, link opens in Chrome or default browser 4. UTM data is captured in your analytics dashboard **Web Browser Behavior** — On desktop or in web browsers: 1. Link opens in default web browser 2. UTM parameters are sent to your server via query string 3. Your analytics platform (GA4, etc.) automatically captures UTM data 4. Users see the content with full tracking context ## What are UTM parameters? UTM (Urchin Tracking Module) parameters are simple text tags you add to URLs that help you track the performance of your marketing campaigns. They tell your analytics platform where traffic came from, what channel it used, and which campaign it belonged to. **Before UTM Parameters:** ```text https://example.com/products ``` You can see that traffic came to your site, but have no idea which campaign drove it or what channel it came from. **With UTM Parameters:** ```text https://example.com/products?utm_source=facebook&utm_medium=paid&utm_campaign=summer_sale ``` Now you know the traffic came from Facebook, via paid ads, as part of your summer sale campaign. ### utm_source Identifies where the traffic came from. Required. Examples: google, facebook, instagram, email, newsletter ### utm_medium Describes the type of marketing channel. Required. Examples: cpc, email, organic, paid, social, referral ### utm_campaign Names the campaign for analysis. Required. Examples: summer_sale, q1_promo, welcome_series ### utm_term Identifies paid search keywords. Optional, mainly for PPC campaigns. Examples: running shoes, best_pizza, mobile_apps ### utm_content Distinguishes different ads or versions within a campaign. Optional, useful for A/B testing. Examples: banner_v1, cta_button_blue, email_subject_a ## UTM parameters with mobile deep links UTM parameters work perfectly with mobile deep links. When you add UTM parameters to a deep link, your app receives the campaign tracking data, allowing you to understand which campaigns drive the most valuable traffic. ### Mobile Deep Link with UTM Parameters Example 1: iOS Universal Link ```text https://myapp.example.com/products/shoes?utm_source=instagram&utm_medium=paid&utm_campaign=summer_sale ``` Example 2: Android App Link ```text https://myapp.example.com/checkout?item=shoes&utm_source=google&utm_medium=cpc&utm_campaign=q1_2024 ``` Example 3: Custom Scheme Deep Link ```text myapp://products/shoes?utm_source=email&utm_medium=newsletter&utm_campaign=weekly ``` ### Mobile App Tracking Flow 1. User sees your ad (Instagram, email, etc.) 2. They tap the deep link with UTM parameters 3. Your app receives the full URL with UTM data 4. Your analytics SDK captures the UTM parameters 5. Data is sent to your analytics platform (GA4, Mixpanel, etc.) 6. You can measure campaign ROI and optimize future campaigns ### Why UTMs Matter for Apps - Track which campaigns drive the most installs - Measure the quality of traffic from each source - Optimize ad spend across channels - Build cohorts of users from specific campaigns - Understand user retention by acquisition channel ## UTM best practices for mobile apps ### 1. Use consistent naming conventions Establish a naming standard for your organization and stick to it. Use lowercase, avoid spaces, and use underscores or hyphens to separate words. Good examples: ```text utm_source=google, utm_medium=cpc, utm_campaign=summer_sale_2024 utm_source=instagram, utm_medium=paid, utm_campaign=product_launch ``` ### 2. Never use spaces or special characters Spaces and special characters can break URLs or get encoded differently across platforms. Always URL-encode parameters properly. - Bad: `utm_campaign=summer sale 2024` - Good: `utm_campaign=summer_sale_2024` ### 3. Document your UTM strategy Create a shared document with your team defining what each UTM parameter means. This ensures consistency and makes reporting easier. Example UTM strategy: - utm_source: Which platform (google, facebook, email, etc.) - utm_medium: Channel type (cpc, organic, email, social, referral) - utm_campaign: Campaign identifier (summer_sale, q2_launch, etc.) - utm_content: Test variant (banner_v1, email_variant_a, etc.) ### 4. Test UTM parameters before launching Use the [Deep Link Tester tool](https://redirectly.app/tools/deep-link-tester.md) to verify that your UTM parameters are being captured correctly by your app. ### 5. Keep UTM parameters short Long URLs can be ugly and hard to share. Some platforms have URL length limitations. Keep values concise while still being descriptive. ### 6. Don't over-complicate the structure You don't need to include every bit of metadata in UTM parameters. Focus on the main dimensions you'll actually analyze and act on. ## Build and track campaigns at scale Use Redirectly to build, manage, and track all your campaign links in one place: - [Use: Deep Link Generator](https://redirectly.app/tools/deep-link-generator.md) - [Test: Deep Link Tester](https://redirectly.app/tools/deep-link-tester.md) - [Learn: UTM Parameters Guide](https://redirectly.app/blog/utm-parameters-mobile-apps.md) ## Frequently asked questions ### What are UTM parameters? UTM (Urchin Tracking Module) parameters are text tags you add to URLs to track campaign performance. They help your analytics platform understand where traffic came from, what marketing channel it used, and which campaign it belonged to. ### How do UTM parameters work with mobile deep links? When a deep link with UTM parameters is tapped, your app receives the full URL including the UTM data. Your analytics SDK then captures these parameters and sends them to your analytics provider for tracking. ### What UTM parameters are required? The three essential parameters are utm_source (where traffic came from), utm_medium (type of marketing channel), and utm_campaign (campaign name or ID). The other two parameters (utm_term and utm_content) are optional. ### Can I use the same UTM parameters across web and app links? Yes! Using consistent UTM parameters across web and app campaigns helps you track user behavior across different channels. Your analytics platform will combine the data for a holistic view. ### How do I view UTM data in my analytics platform? This depends on your analytics platform (GA4, Mixpanel, Amplitude, etc.). Most platforms have built-in reports for UTM parameters. Your mobile analytics SDK automatically captures and sends the UTM data from deep links. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Apple App Site Association (AASA) Validator > Validate your apple-app-site-association file for iOS Universal Links. Check DNS, HTTPS, content-type, JSON format, and signing requirements instantly. - Canonical: https://redirectly.app/aasa-validator - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Test that your domain serves a valid apple-app-site-association file for Universal Links. We run the same checks Apple uses and show you the parsed JSON. ## Validate Your AASA File This is an interactive tool: enter your domain at [https://redirectly.app/aasa-validator](https://redirectly.app/aasa-validator) to validate your apple-app-site-association file. The validator checks DNS resolution, HTTPS connectivity, Content-Type headers, and JSON syntax. ## What is an Apple App Site Association (AASA) File? An Apple App Site Association (AASA) file is a JSON configuration file that iOS and macOS use to verify that your app owns a particular web domain. By hosting this file on your domain, you allow your app to handle Universal Links—HTTPS URLs that open your app instead of Safari when tapped. The AASA file is a critical part of iOS deep linking infrastructure. Without it, links to your domain will open in Safari instead of launching your app. Apple downloads and validates the AASA file from your domain during app installation, and periodically refreshes it to ensure your app configuration is still valid. The file must be placed at `/.well-known/apple-app-site-association` on your domain, served over HTTPS, and return the correct `Content-Type` header. - **Domain Verification** — AASA files prove to iOS that your app is associated with your domain, preventing other apps from hijacking your links. - **Universal Links** — Enable HTTPS links that seamlessly open your app with zero fallback to the browser when the app is installed. ## How Universal Links Work Universal Links are the secure, modern way to deep link on iOS. When a user taps a Universal Link (an HTTPS URL), iOS checks if the associated domain has an AASA file that claims the app as an owner. If verified, the link opens the app directly. If the app is not installed, the link opens in Safari. ### The Universal Links Flow 1. **User Installs Your App** — iOS downloads the AASA file from your domain and validates it. 2. **iOS Validates AASA** — Apple checks the file syntax, content type, and your app's bundle ID. 3. **User Taps a Link** — When the user taps an https:// link to your domain, iOS checks if your app is registered. 4. **App Opens or Falls Back** — If your app is installed and claims the domain, it opens. Otherwise, Safari opens the URL. **Security Benefit:** Unlike custom schemes (myapp://), Universal Links are verified by domain ownership. Apple never routes a Universal Link to the wrong app, eliminating URL scheme hijacking attacks. ## AASA JSON Format Reference The AASA file is a JSON file with a specific structure. Here's what a typical AASA file looks like and what each field means. ```json { "applinks": { "apps": [], "details": [ { "appID": "ABCD123456.com.example.myapp", "paths": [ "/account/*", "/product/*", "NOT /store/*" ] } ] }, "webcredentials": { "apps": ["ABCD123456.com.example.myapp"] } } ``` ### appID Format Your app ID consists of your Team ID (from Apple Developer Account) followed by your Bundle Identifier: ```text ABCD123456.com.example.myapp ``` Team ID (10 chars) + Bundle Identifier = appID ### Paths Use wildcard patterns to specify which paths your app handles: - `/product/*` — Matches /product/123, /product/abc, etc. - `NOT /store/*` — Excludes /store/ paths from your app - `/*` — Matches all paths ### Sections - **applinks** — Handles Universal Links to your domain - **webcredentials** — For password managers and autofill (optional) - **appclips** — For App Clips (iOS 14+, optional) ## How to Create an AASA File from Scratch ### 1. Find Your App ID Log in to your Apple Developer Account and go to Identifiers. Find your App ID in the format `ABCD123456.com.example.myapp`. The first 10 characters are your Team ID. ### 2. Create the JSON File Create a file named `apple-app-site-association` (no .json extension) with this content: ```json { "applinks": { "apps": [], "details": [ { "appID": "YOUR_TEAM_ID.YOUR_BUNDLE_ID", "paths": ["/"] } ] } } ``` Replace YOUR_TEAM_ID and YOUR_BUNDLE_ID with your actual values. ### 3. Upload to Your Server Place the file at `/.well-known/apple-app-site-association` on your domain. For example: `https://redirectly.app/.well-known/apple-app-site-association` ### 4. Configure Server Headers Ensure your server returns the correct Content-Type header: ```text Content-Type: application/json ``` ### 5. Add Associated Domains to Your App In Xcode, go to your app target Signing & Capabilities, click "+ Capability" and select Associated Domains. Add your domain with the applinks prefix: ```text applinks:redirectly.app ``` ### 6. Validate with This Tool Use the AASA Validator to check your file is correctly configured and accessible. ## Common AASA Errors and How to Fix Them ### Wrong Content-Type Header **Problem:** Server returns text/plain or text/html instead of application/json. **Fix:** Update your web server configuration. For Apache, add to .htaccess: AddType application/json .well-known/apple-app-site-association. For Nginx, add to your config: types { application/json apple-app-site-association; } ### File Not Found (404) **Problem:** File is in the wrong location or domain is unreachable. **Fix:** Ensure the file is at /.well-known/apple-app-site-association on the root of your domain. Verify your domain is accessible over HTTPS without redirects. ### Invalid App ID **Problem:** appID format is incorrect or missing Team ID. **Fix:** Use the format XXXXXXXXXX.bundleidentifier where X is your 10-character Team ID. Example: ABCD123456.com.example.myapp ### Malformed JSON **Problem:** JSON syntax errors prevent parsing. **Fix:** Validate your JSON using a JSON validator. Check for missing commas, extra quotes, or unclosed brackets. ### HTTPS Redirect Issues **Problem:** Domain redirects from HTTP to HTTPS or to a different domain. **Fix:** Apple requires the AASA file to be directly accessible on the exact domain without any redirects. Remove any redirect rules that might affect the /.well-known/ path. ### Associated Domains Not Enabled **Problem:** App has no Associated Domains capability in Xcode. **Fix:** In Xcode, select your app target, go to Signing & Capabilities, click + Capability, and add Associated Domains. Add applinks:yourdomain.com for each domain. ### Path Patterns Not Matching **Problem:** Links to paths not included in the AASA file don't open the app. **Fix:** Update the paths array in your AASA file. Use * for wildcards (e.g., /product/*) and NOT to exclude paths (e.g., NOT /admin/*). ### File Cached Incorrectly **Problem:** Changes to the AASA file don't take effect. **Fix:** Apple caches the AASA file. Reinstall your app on the device, or wait up to 24 hours for the cache to refresh. Clear app data as a workaround. ## Frequently Asked Questions ### Is the AASA file required to use Universal Links? Yes. Without a valid AASA file on your domain, iOS cannot verify your app owns the domain and will not route Universal Links to your app. The file must be correctly formatted, placed, and served with the right headers. ### How often does Apple check the AASA file? Apple downloads the AASA file when the app is installed and periodically refreshes it (typically daily). Changes may take up to 24 hours to propagate, though reinstalling the app forces an immediate refresh. ### Can I use the same AASA file for multiple apps? Yes. Add multiple appID entries in the details array, each with the appropriate bundle identifier and paths. This allows multiple apps to claim the same domain with different path handling. ### What happens if my domain expires or SSL cert lapses? If your domain becomes inaccessible, iOS cannot verify the AASA file and will stop routing Universal Links to your app. Users will see links open in Safari instead. Restore HTTPS access immediately to restore Universal Links. ### Do I need to test Universal Links on a real device? Yes. Universal Links only work on actual iOS devices, not simulators. To test, share a link via SMS or email, tap it from a different app, and verify your app opens instead of Safari. ### Can I use subdomains with Universal Links? Yes. Each subdomain needs its own Associated Domain entry in Xcode (applinks:subdomain.example.com) and a valid AASA file on that subdomain. Subdomains are treated independently. ### What if I need to migrate to a new domain? Set up the AASA file on the new domain with your app's bundle ID and paths. Update your app to include the new domain in Associated Domains. You can keep the old domain's AASA file for backwards compatibility during the transition. ### Is there a maximum size for the AASA file? There is no official limit, but keep it minimal. Most AASA files are under 10KB. Avoid adding unnecessary paths or app IDs to keep the file lightweight. ## Related Tools & Resources - [Asset Links Validator](https://redirectly.app/assetlinks-validator.md) — Validate assetlinks.json for Android App Links and Digital Asset Links. - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) — Complete guide to Universal Links, App Links, and deferred deep linking. - [Flutter Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Implement deferred deep linking in Flutter with Redirectly SDK. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # AssetLinks.json Validator > Validate your assetlinks.json file for Android App Links. Free tool to check DNS, HTTPS, JSON format, SHA-256 fingerprints, and Digital Asset Links setup. - Canonical: https://redirectly.app/assetlinks-validator - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native Validate your Android App Links configuration. Check DNS, HTTPS, content-type, JSON format, SHA-256 fingerprints, and package name instantly. Debug Digital Asset Links setup in seconds. ## Validate Your assetlinks.json This is an interactive tool: enter your domain at [https://redirectly.app/assetlinks-validator](https://redirectly.app/assetlinks-validator) and it checks that your assetlinks.json file is properly hosted, has correct headers, and contains valid JSON. ## What is Digital Asset Links (assetlinks.json)? Digital Asset Links is a Google-developed security mechanism that establishes verified relationships between Android apps and websites. At its core is the `assetlinks.json` file—a simple JSON configuration that tells Android that your app is authorized to handle links for your domain. When you implement Digital Asset Links correctly, Android can automatically open qualifying links directly in your app without showing the app chooser dialog. This seamless experience is called **Android App Links**, and it's essential for modern app marketing and user experience. **Key Benefit:** Android App Links eliminate friction by automatically routing verified links to your app, improving user experience and reducing navigation abandonment. Without proper Digital Asset Links setup, users see the ambiguous "Open with?" dialog when clicking links to your domain from outside your app, forcing them to manually select your app every time. ## How Android App Links Work Android App Links work through a three-part verification process involving your app's manifest, the assetlinks.json file, and Android's validation: 1. **App Declares Intent Filter** — Your AndroidManifest.xml declares intent filters for specific URL schemes (e.g., `https://example.com`) with `android:autoVerify="true"`. 2. **Android Fetches assetlinks.json** — When the app is installed, Android automatically fetches your domain's `/.well-known/assetlinks.json` file over HTTPS to verify the relationship. 3. **Android Verifies Signature** — Android compares the SHA-256 fingerprint of your app's signing certificate (in assetlinks.json) with the app's actual certificate. If they match, the domain is verified. 4. **Links Open Directly** — Once verified, Android automatically routes matching links directly to your app, bypassing the app chooser dialog and creating a seamless user experience. **Important:** The assetlinks.json file must be served over HTTPS, have the correct Content-Type header (`application/json`), and be publicly accessible. Android can't use HTTP or private files. ## assetlinks.json Format Reference The assetlinks.json file is a JSON array containing relation objects that map between your website and app. Here's the complete structure: ```json [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app", "package_name": "com.example.android", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99", "11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00" ] } } ] ``` - **relation array** — `"delegate_permission/common.handle_all_urls"` means your app is authorized to handle all URLs for the domain without showing the app chooser. This is the standard relation for Android App Links. - **namespace** — Must be `"android_app"` for Android apps. (For iOS, you'd use `"web"` in the AASA file.) - **package_name** — Your app's package name exactly as defined in AndroidManifest.xml (e.g., `com.example.android`). Must be case-sensitive and exact. - **sha256_cert_fingerprints** — Array of SHA-256 certificate fingerprints for your app's signing keys. Include fingerprints for all signing certificates you use (debug, release, etc.). Each fingerprint is a 64-character hexadecimal string in uppercase with colons separating each byte pair. **Pro Tip:** You can include multiple relations and targets in the same assetlinks.json file to support multiple apps or multiple signing certificates, making it flexible for your deployment strategy. ## How to Create assetlinks.json from Scratch 1. **Get Your SHA-256 Fingerprints** — Obtain the SHA-256 fingerprints for your debug and release signing certificates (see the section below). 2. **Create the JSON File** — Create a file named `assetlinks.json` with the format shown in the reference section above. Use the exact package name and SHA-256 fingerprints. 3. **Create .well-known Directory** — On your web server, create a directory named `.well-known` at the root of your domain if it doesn't exist. 4. **Upload the File** — Upload assetlinks.json to `/.well-known/assetlinks.json`. The full URL should be accessible at `https://example.com/.well-known/assetlinks.json` 5. **Configure Server Headers** — Ensure your server returns `Content-Type: application/json` for the assetlinks.json file. Set appropriate cache headers (usually 1 day or longer). 6. **Update AndroidManifest.xml** — In your app's AndroidManifest.xml, add intent filters with `android:autoVerify="true"` for your domain's URLs: ```xml ``` 7. **Test with Validator** — Use this assetlinks.json validator to confirm your file is correctly configured and accessible. Fix any issues reported before distributing your app. ## How to Get Your SHA-256 Certificate Fingerprint The SHA-256 certificate fingerprint is crucial for Android App Links. You need fingerprints for both debug (development) and release (production) signing certificates. Here are the three methods: ### Method 1: Using keytool (Recommended) Use the Java keytool utility included with your JDK: ```bash keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android ``` For release keystore: ```bash keytool -list -v -keystore /path/to/my-release-key.jks -alias my-key-alias ``` Look for the "SHA256:" line in the output. Copy only the 64-character hexadecimal part (after "SHA256: ") and convert it to the colon-separated format for assetlinks.json. ### Method 2: From Google Play Console If your app is already published on Google Play: 1. Go to your app in Google Play Console 2. Navigate to Settings > App signing 3. Find "App signing certificate" 4. Click "Details" to view the SHA-256 fingerprint 5. Copy the SHA-256 value (already in colon-separated format) **Note:** This is your release certificate fingerprint. For debug builds, you still need to use keytool with your debug keystore. ### Method 3: Using Android Studio In Android Studio: 1. Open Build > Analyze APK 2. Select your signed APK 3. Navigate to the Certificates tab 4. Look for the SHA-256 fingerprint in the certificate details **Critical:** You need *both* debug and release SHA-256 fingerprints in assetlinks.json. The debug fingerprint is for development testing, and the release fingerprint is for production apps distributed via Google Play. ## Common assetlinks.json Errors and How to Fix Them ### Error: File Not Found (404) **Problem:** assetlinks.json is not accessible at /.well-known/assetlinks.json **Solution:** - Verify the file exists at the correct path: `https://your-domain.com/.well-known/assetlinks.json` - Check that the .well-known directory exists and is readable - Ensure file permissions allow web server access - Verify the filename is spelled correctly (case-sensitive) ### Error: Wrong Content-Type **Problem:** Server returns wrong Content-Type (e.g., text/plain or text/html instead of application/json) **Solution:** - Configure your web server to serve assetlinks.json with `Content-Type: application/json` - For Apache: Add `AddType application/json .json` to .htaccess - For Nginx: Add `types { application/json json; }` to config - For Node.js/Express: Use middleware like express-static with proper MIME type configuration ### Error: HTTP Instead of HTTPS **Problem:** assetlinks.json is served over HTTP instead of HTTPS **Solution:** - Configure your domain to use HTTPS with a valid SSL certificate - Redirect all HTTP traffic to HTTPS - Android App Links require HTTPS for security reasons - Use a service like Let's Encrypt for free SSL certificates ### Error: Invalid JSON Format **Problem:** assetlinks.json contains malformed JSON (syntax errors) **Solution:** - Validate JSON syntax using an online JSON validator - Check for missing commas between array/object elements - Ensure all quotes are properly closed and escaped - Verify no trailing commas exist in arrays or objects - Use a code editor with JSON syntax highlighting ### Error: Wrong SHA-256 Fingerprint **Problem:** SHA-256 fingerprint in assetlinks.json doesn't match your app's signing certificate **Solution:** - Re-verify your SHA-256 fingerprint using keytool or Google Play Console - Ensure format is correct: 64-char hex with colons every 2 characters - Include fingerprints for BOTH debug and release certificates - Check that you're using the right keystore file - Verify the app you're testing is signed with the certificate in assetlinks.json ### Error: Wrong Package Name **Problem:** Package name in assetlinks.json doesn't match your app's AndroidManifest.xml **Solution:** - Open your AndroidManifest.xml and find the exact package name in the manifest tag - Update assetlinks.json with the exact same package name - Package names are case-sensitive - Don't include activity or service names; use only the root package ### Error: No Automatic App Opening **Problem:** Links still show app chooser despite valid assetlinks.json **Solution:** - Ensure AndroidManifest.xml has `android:autoVerify="true"` - Uninstall the app completely and reinstall fresh from Google Play or signed APK - Android verifies assetlinks.json during installation; reinstalling re-triggers verification - Clear app data and cache, then reinstall - Wait 24-48 hours for Play Console to update distribution if recently changed ### Error: Multiple Signing Certificates Not Included **Problem:** assetlinks.json only includes release fingerprint, but debug builds fail **Solution:** - Add both debug and release SHA-256 fingerprints to the `sha256_cert_fingerprints` array - This allows both development testing and production builds to work - Include fingerprints for any other signing certificates you use ## Frequently Asked Questions ### What is assetlinks.json and why do I need it? assetlinks.json is a security configuration file that establishes a verified relationship between your Android app and website domain. Without it, Android shows an app chooser dialog when users click links to your domain. With proper assetlinks.json setup, Android automatically opens qualifying links directly in your app, creating a seamless user experience. This automatic behavior is called Android App Links. ### Where exactly should I place the assetlinks.json file? The file must be placed at `https://your-domain.com/.well-known/assetlinks.json`. The .well-known directory must be at the root of your domain, and the file must be publicly accessible and served over HTTPS with the Content-Type header set to `application/json`. ### What is a SHA-256 certificate fingerprint and how do I get it? A SHA-256 certificate fingerprint is a unique 64-character hexadecimal identifier for your app's signing certificate. Android uses it to verify that your app is authorized to handle links for your domain. You can get it using three methods: (1) the keytool command with your keystore file, (2) Google Play Console for release certificates, or (3) Android Studio's APK analyzer. You need fingerprints for both debug and release certificates. ### Why aren't my Android App Links working even though assetlinks.json looks correct? Common causes include: (1) wrong or mismatched SHA-256 fingerprint, (2) wrong package name, (3) missing `android:autoVerify="true"` in AndroidManifest.xml, (4) assetlinks.json not served over HTTPS, (5) incorrect Content-Type header, or (6) the app was installed before assetlinks.json was deployed. Try uninstalling and reinstalling the app, and use this validator to check for configuration issues. ### Can I have multiple apps or packages in the same assetlinks.json file? Yes, assetlinks.json is an array that can contain multiple relation objects. You can include entries for multiple apps (different package names) and multiple signing certificates (different SHA-256 fingerprints). This is useful if you have multiple apps using the same domain, or multiple signing keys across different build flavors or environments. ### Do I need both debug and release SHA-256 fingerprints in assetlinks.json? Yes, if you want Android App Links to work for both development and production. The debug fingerprint is for testing on development devices with unsigned/debug builds. The release fingerprint is for the production app distributed via Google Play. Include both in the `sha256_cert_fingerprints` array as separate entries. ### What is the difference between assetlinks.json and AASA? assetlinks.json (Digital Asset Links) is for Android App Links and is published by Android apps on their website domain. AASA (apple-app-site-association) is for iOS Universal Links and is hosted on the website. Both achieve the same goal on different platforms: allowing apps to claim ownership of links to their domain without showing a chooser dialog. Websites often host both files to support Android and iOS app linking. ### How can I test if my assetlinks.json is working correctly? Use this free assetlinks.json validator to check your configuration immediately. For real-world testing, install your app on an Android device and click links to your domain in a browser or from another app. If configured correctly, the link will open directly in your app without showing the app chooser. You can also check the Android system logs for verification messages. Allow 24-48 hours for Play Console to update distribution if you recently changed signing certificates. ## Related Resources - [AASA Validator (iOS)](https://redirectly.app/aasa-validator.md) — Validate apple-app-site-association files for iOS Universal Links, just like this tool validates Android App Links. - [Complete Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) — Learn deep linking fundamentals for both Android and iOS, including best practices and implementation strategies. - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Implement deferred deep linking in Flutter apps to handle install attribution and post-install routing. - [Redirectly Home](https://redirectly.app/index.md) — Learn about smart deferred deep linking and how Redirectly helps with install attribution and post-install routing. ## Ready to Implement Android App Links? Now that you understand assetlinks.json and how to set it up, use the validator to check your configuration. If you need more advanced features like deferred deep linking and install attribution, Redirectly provides a complete solution. Sign up for a free API key and subdomain to get started. Android App Links are a powerful feature that improve user experience by seamlessly routing domain links to your app. With proper assetlinks.json configuration, you eliminate friction and increase engagement. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly Blog > Expert articles on deferred deep linking, Flutter and React Native integration, install attribution, and Firebase Dynamic Links migration. - Canonical: https://redirectly.app/blog - [Best Firebase Dynamic Links Alternative 2026](https://redirectly.app/blog/best-firebase-dynamic-links-alternative-2026.md) — Comparison, 2026-01-08: An honest roundup comparing Redirectly, Branch, Dub, AppsFlyer, and Adjust to find the best Firebase Dynamic Links alternative for indie developers. - [Vibe Coding with Deep Links](https://redirectly.app/blog/vibe-coding-deep-links.md) — AI Tools, 2025-10-07: How AI-native development is changing the developer workflow, and how MCP servers automate deep linking through AI editor integration and tooling. - [Build Deep Links with AI Editors & MCP](https://redirectly.app/blog/redirectly-mcp-server-ai-editors.md) — AI Tools, 2025-09-16: Leverage AI editors to build deep links with Redirectly's MCP server. Setup guides for Cursor, Claude Desktop, VS Code, and more for vibe coding. - [Set Up Deep Linking with Cursor + MCP](https://redirectly.app/blog/cursor-mcp-deep-linking.md) — AI Tools, 2025-09-02: A complete walkthrough to install the MCP server, configure Cursor, ask it to set up deep links, and verify results with AI-assisted automation. - [Firebase Dynamic Links Shutdown Guide](https://redirectly.app/blog/firebase-dynamic-links-shutdown.md) — Migration, 2025-08-26: Firebase Dynamic Links shut down in August 2025. Learn the timeline, what broke in your apps, and discover better alternatives like Redirectly. - [UTM Parameters for Mobile Apps](https://redirectly.app/blog/utm-parameters-mobile-apps.md) — Marketing, 2025-07-08: Learn how to structure UTM parameters for deep links, preserve them through install, and read UTM data in Flutter and React Native apps. - [Branch vs AppsFlyer vs Adjust Compared](https://redirectly.app/blog/branch-vs-appsflyer-vs-adjust.md) — Comparison, 2025-06-17: Compare Branch, AppsFlyer, and Adjust for deep linking and attribution. See pricing, SDK support, deferred deep linking, and a lightweight alternative. - [Mobile Install Attribution Guide](https://redirectly.app/blog/mobile-install-attribution-guide.md) — Attribution, 2025-05-06: A complete guide to mobile install attribution covering attribution models, Google Play Referrer, iOS SKAN, and key privacy considerations. - [Flutter Deep Linking with go_router](https://redirectly.app/blog/flutter-deep-linking-go-router.md) — Flutter, 2025-04-15: Master Flutter deep linking with go_router. Learn route configuration, path parameters, redirect guards, and deferred deep link handling. - [Universal Links in React Native (iOS)](https://redirectly.app/blog/react-native-universal-links-ios.md) — React Native, 2025-04-01: Master Universal Links in React Native with Xcode configuration, AASA file setup, entitlements, AppDelegate code, and real device testing on iOS. - [React Native Firebase Links Migration](https://redirectly.app/blog/react-native-firebase-dynamic-links-migration.md) — React Native, 2025-03-25: A step-by-step React Native guide to migrate from Firebase Dynamic Links to Redirectly, with TypeScript code examples, config, and troubleshooting. - [assetlinks.json Guide for Android App Links](https://redirectly.app/blog/assetlinks-json-guide.md) — Android, 2025-03-11: Master assetlinks.json for Android App Links. Learn Digital Asset Links format, SHA-256 certificate fingerprints, and hosting requirements. - [AASA File Format for iOS Universal Links](https://redirectly.app/blog/aasa-file-format-ios-guide.md) — iOS, 2025-03-04: Master the Apple App Site Association (AASA) file format. Learn JSON structure, path patterns, and hosting requirements for iOS universal links. - [React Native Deep Linking with Navigation](https://redirectly.app/blog/react-native-deep-linking-react-navigation.md) — React Native, 2025-02-25: Master deep linking in React Native with React Navigation v6. Learn linking config, screen mapping, TypeScript examples, and testing strategies. - [Firebase Dynamic Links vs Redirectly](https://redirectly.app/blog/firebase-dynamic-links-vs-redirectly.md) — Comparison, 2025-02-11: A complete feature-by-feature comparison of Firebase Dynamic Links and Redirectly, covering pricing, code examples, and which is the better fit. - [Set Up Android App Links in Flutter](https://redirectly.app/blog/flutter-android-app-links-setup.md) — Flutter, 2025-01-21: Implement Android App Links in Flutter with AndroidManifest.xml config, assetlinks.json setup, Dart code, and SHA-256 fingerprint generation. - [Set Up Universal Links in Flutter (iOS)](https://redirectly.app/blog/flutter-universal-links-ios-setup.md) — Flutter, 2025-01-14: Set up Universal Links in Flutter for iOS with step-by-step Xcode configuration, AASA file creation, Dart implementation, and troubleshooting. - [Deep Links vs Universal Links vs App Links](https://redirectly.app/blog/deep-linking-vs-universal-links-vs-app-links.md) — Fundamentals, 2024-12-10: Learn the differences between deep linking, Universal Links on iOS, and App Links on Android, plus URI schemes and when to use each one. - [Deferred vs Regular Deep Links](https://redirectly.app/blog/deferred-vs-regular-deep-link.md) — Fundamentals, 2024-12-03: Understand the difference between deferred and regular deep links. Learn when to use each, implementation patterns, and user experience trade-offs. - [Debugging Deep Links in Flutter](https://redirectly.app/blog/debugging-deep-links-flutter.md) — Flutter, 2024-11-19: Fix Universal Links, App Links, and deferred deep links in Flutter. A step-by-step debugging guide with solutions for the most common issues. - [How to Migrate from Firebase Dynamic Links to Redirectly | Complete Guide](https://redirectly.app/blog/migrating-from-firebase-dynamic-links.md) — Migration, 2024-02-15: Step-by-step guide to migrating your Flutter app from Firebase Dynamic Links to Redirectly. Keep your deep links working with our drop-in replacement SDK. - [What is Deferred Deep Linking? The Complete Guide](https://redirectly.app/blog/what-is-deferred-deep-linking.md) — Fundamentals, 2024-01-22: Learn how deferred deep linking solves the attribution problem in mobile app marketing and user acquisition. - [What is Deep Linking? A Complete Guide for Developers](https://redirectly.app/blog/what-is-deep-linking.md) — Fundamentals, 2024-01-20: Learn the fundamentals of deep linking, how it works, and why it's essential for modern mobile app development. - [Email Verification with Deep Linking: Complete Implementation Guide](https://redirectly.app/blog/email-verification-deep-linking.md) — Authentication, 2024-01-15: Learn how to implement secure email verification using temporary deep links that work seamlessly across web and mobile platforms. - [Secure Password Reset for Flutter Apps: A Complete Guide](https://redirectly.app/blog/password-reset-flutter-apps.md) — Security, 2024-01-12: Implement secure password reset flows using temporary links that work perfectly with your Flutter app's deep linking system. - [Building a Referral Program with Deep Linking and Install Attribution](https://redirectly.app/blog/referral-program-deep-linking.md) — Growth, 2024-01-10: Create a powerful referral system that tracks installs, attributes users to referrers, and provides seamless onboarding experiences. - [Marketing Campaigns with Deep Linking: From Click to Conversion](https://redirectly.app/blog/marketing-campaigns-deep-linking.md) — Marketing, 2024-01-08: Maximize your marketing ROI with deep linking campaigns that provide seamless user experiences and detailed attribution tracking. - [App Onboarding with Deferred Deep Linking: Complete User Journey](https://redirectly.app/blog/app-onboarding-deferred-deep-linking.md) — UX, 2024-01-05: Design perfect onboarding experiences that work even when users install your app after clicking a marketing link. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Best Firebase Dynamic Links Alternative in 2026 > An honest roundup comparing Redirectly, Branch, Dub, AppsFlyer, and Adjust to find the best Firebase Dynamic Links alternative for indie developers. - Canonical: https://redirectly.app/blog/best-firebase-dynamic-links-alternative-2026 - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2026-03-07 - Author: Dmytro Vyrych - Category: Comparison · 8 min read Firebase deprecated Dynamic Links in 2025. We compare Redirectly, Branch, Dub, AppsFlyer, and Adjust to help you find the best replacement for your app. ## Why You Need a Firebase Alternative Firebase Dynamic Links powered millions of app deep links for over a decade. But in 2025, Google deprecated it, giving developers until late 2026 to migrate. You have options. The question isn't whether to migrate—it's which platform fits your needs and budget. We'll walk through the major players and give you an honest assessment for different use cases. **Disclosure:** This is a comparison by Redirectly. We've aimed to be fair and accurate, but we obviously think Redirectly is the best choice for most indie developers. Read on and decide for yourself. ## Platform Comparison Table | Feature | Redirectly | Branch | Dub | AppsFlyer | Adjust | | --- | --- | --- | --- | --- | --- | | Free Tier | Yes | Yes | Yes | No | No | | No Code Quota | Yes | No | Yes | No | No | | Flutter Support | Yes | Yes | Yes | Yes | Yes | | React Native Support | Yes | Yes | Yes | Yes | Yes | | Analytics | Basic | Advanced | Basic | Advanced | Advanced | | API Documentation | Excellent | Good | Good | Good | Good | | MCP Server | Yes | No | No | No | No | | Best For | Indie Devs | Enterprise | Marketers | Enterprise | Enterprise | ## Detailed Platform Reviews ### Redirectly - Built for Indie Developers Best for: Flutter/React Native indie developers **Pricing:** Free tier unlimited, $29/mo for advanced features **Setup Time:** 5-10 minutes with MCP, 30 minutes manually **Learning Curve:** Very gentle—excellent documentation **Strengths:** - Free tier truly unlimited (no quota) - MCP server for AI-native setup (game changer) - Built specifically for mobile development - Simple, clean dashboard - Excellent documentation and tutorials - Transparent pricing with no surprises **Weaknesses:** - Less advanced analytics than competitors - Smaller team means less immediate support - Fewer integrations (but growing) ### Branch - The Enterprise Choice Best for: Large teams and enterprises **Pricing:** Free for testing, enterprise pricing starts $4k+/year **Setup Time:** 30-60 minutes **Learning Curve:** Steep—lots of features **Strengths:** - Industry leader with deep platform integration - Advanced analytics and attribution - Extensive partner integrations - Strong customer support - Proven reliability at scale **Weaknesses:** - Expensive for indie developers ($4k+ annually) - Feature overload if you just need deep links - Slower setup process - Dashboard is complex - Free tier has strict quotas ### Dub - For Link Shortening & Attribution Best for: Marketing teams with complex attribution **Pricing:** Free tier, $20/mo Pro, custom enterprise **Setup Time:** 15 minutes **Learning Curve:** Easy—marketing-focused UI **Strengths:** - Great for URL shortening + tracking - QR code generation built-in - Good marketing analytics - Clean, modern interface - Generous free tier **Weaknesses:** - Not specifically designed for deep linking - Limited mobile app integration - Lacks platform-specific features (iOS/Android configuration) - Better for marketers than developers ### AppsFlyer - Analytics Platform Best for: Large app teams doing cohort analysis **Pricing:** Enterprise pricing only (no free tier) **Setup Time:** 1-2 hours with support **Learning Curve:** Very steep—complex platform **Strengths:** - Industry-standard mobile analytics - Advanced cohort analysis - Fraud detection - White-label options **Weaknesses:** - No free tier—pricing starts $5k+/year - Not ideal if you just need deep links - Overkill for indie developers - Complex setup process ### Adjust - Analytics & Fraud Prevention Best for: Large teams needing fraud detection **Pricing:** Enterprise pricing only (no free tier) **Setup Time:** 1-2 hours with support **Learning Curve:** Steep—complex analytics focus **Strengths:** - Best-in-class fraud detection - Strong privacy compliance (GDPR, etc.) - Excellent customer success team - Used by major apps worldwide **Weaknesses:** - No free tier—enterprise only - Minimum budget required - Complex setup for simple deep linking - Not designed for indie developers ## Who Should Choose What? ### Choose Redirectly If: - You're an indie developer or small team - You want truly unlimited free tier (no quotas) - You use Cursor or another AI editor (MCP) - You primarily build with Flutter or React Native - You want simple, transparent pricing ### Choose Branch If: - You have a large team and substantial budget - You need advanced analytics and attribution - You require extensive integrations - You prefer established enterprise vendor ### Choose Dub If: - Your focus is URL shortening for marketing - You need QR code generation - You're using it for web links, not app deep links ### Choose AppsFlyer/Adjust If: - Your app reaches millions of users - You need enterprise-grade analytics - Fraud detection is critical to your business - You have significant marketing budget ## Annual Cost Comparison (Typical Setup) - **Redirectly — $348/year:** Free tier + $29/mo for advanced features (still 95% cheaper than alternatives) - **Dub — $240/year:** Free tier + $20/mo (but limited deep linking features) - **Branch — $4,000+/year:** Minimum enterprise tier, often $10k+ with typical usage - **AppsFlyer — $5,000+/year:** Enterprise pricing, minimum spend required - **Adjust — $5,000+/year:** Enterprise pricing, minimum spend required **Bottom line:** For indie developers, Redirectly is 10-15x cheaper than competitors while offering better tooling for modern AI-native development. ## Migration Tips from Firebase ### Plan Your Migration Firebase is fully sunsetted in late 2026. Plan your migration now: - Audit all Firebase Dynamic Links in production - Create a redirect strategy for existing shared links - Set up new platform in parallel (don't cut over suddenly) - Test on real devices before going live - Update any documentation or help articles ### Redirectly Migration is Easy If you choose Redirectly, migration takes 30 minutes: ```text 1. Export your Firebase Dynamic Links data 2. Import into Redirectly dashboard 3. Update your routing code 4. Set up SSL certificate verification 5. Test on real devices 6. Switch DNS/links to Redirectly ``` ## Our Final Recommendation If you were using Firebase Dynamic Links, you're likely an indie developer or small team building mobile apps. Redirectly is the clear winner for your use case: it's 10x cheaper, focuses specifically on mobile deep linking, has zero quotas on the free tier, and includes MCP integration for modern AI-native development. Branch is superior if you have a large team and need advanced analytics. AppsFlyer and Adjust are for teams at massive scale needing fraud detection and attribution modeling. For everyone else: start with Redirectly. It's faster to implement, costs less, and as your app scales, you can evaluate enterprise alternatives later. [Learn About Migration](https://redirectly.app/firebase-dynamic-links-alternative.md) ## Related Articles - [Set Up Deep Linking with Cursor + MCP](https://redirectly.app/blog/cursor-mcp-deep-linking.md) — Streamline your setup with AI-native development using MCP servers. - [Branch Alternative for Indie Apps](https://redirectly.app/branch-alternative.md) — Compare Redirectly with Branch for your specific use case. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Vibe Coding with Deep Links > How AI-native development is changing the developer workflow, and how MCP servers automate deep linking through AI editor integration and tooling. - Canonical: https://redirectly.app/blog/vibe-coding-deep-links - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2026-03-07 - Author: Dmytro Vyrych - Category: AI Tools · 8 min read AI-native development isn't just about writing code faster. It's about flowing through your codebase with intention, context, and intuition. Deep linking is where this vision becomes reality. ## The Shift: From Tools to Extensions For decades, developers have treated tools as separate from their workflow. You write code in an editor, then switch context to documentation, then to CLI tools, then back. Each switch costs mental energy and breaks flow. AI editors with MCP support change this fundamentally. Your tools aren't separate anymore—they're extensions of your thought process. When you need to set up deep linking, you don't navigate to another tool. You ask your editor, and it handles everything while you stay in context. **The Vibe Coding Experience:** "Vibe coding" captures this seamless state. You're not thinking about the mechanics of your tools—you're thinking about your feature. The AI understands your codebase context and makes the right implementation decisions instantly. No research, no switching windows, no manual configuration. Just flow. ## How MCP Servers Enable This Experience ### Context Awareness MCP servers give AI editors visibility into your exact project structure, dependencies, and configuration. The AI doesn't generate generic boilerplate—it generates code tailored to your setup. ```text Before MCP: "Add deep linking to a Flutter app" → Generic, assumes standard setup, may not match your project After MCP: "Add deep linking to a Flutter app" → AI inspects your pubspec.yaml, structure, platform setup → Generates exact code for YOUR app ``` ### Real-Time Verification The MCP server can validate your configuration in real-time. Did you set up the AASA file correctly? Is your domain properly registered? The AI knows and tells you immediately, not after deploy. This eliminates the painful discovery phase where you only learn about issues through user reports or extensive manual testing. ### Knowledge Integration MCP servers can encode best practices, platform-specific knowledge, and latest standards. Your AI editor becomes an expert on deep linking for every platform. Instead of you researching Apple's latest iOS requirements, the MCP server knows them and applies them automatically. ## The Developer Experience Transformation ### Before: The Traditional Workflow 1. Google "how to set up deep linking Flutter" 2. Read 5 different blog posts with conflicting advice 3. Open Xcode and manually configure Associated Domains 4. Create AASA file, unsure if format is correct 5. Write boilerplate Dart code for link handling 6. Test on physical device (wait 10+ minutes) 7. Debug cryptic errors from incompatible configs 8. Finally ship, hoping you didn't miss anything Total time: 2-4 hours ### After: The AI-Native Workflow 1. In Cursor: "@redirectly Set up deep linking for myapp.com" 2. AI analyzes your codebase (instant) 3. Generates and applies all platform-specific code 4. Validates AASA file and assetlinks.json 5. Provides testing commands (copy-paste ready) 6. Explains what it did and why Total time: 5 minutes ### The Real Win: Confidence You don't just save time. You gain confidence that your implementation is correct because it was generated by AI with access to verified knowledge bases and real-time validation. No guessing, no hoping. ## Example Workflows with AI Editors ### Workflow 1: Quick Feature Addition You're building a referral feature and need deep links for share codes: ```text You: "I need to add share links to my app. When someone clicks share://code123, open the referral screen with that code." Cursor (with @redirectly): - Understands your routing setup - Creates deep link handlers for share:// scheme - Adds validation and error handling - Tests the implementation Result: Feature done in minutes, shipping with confidence ``` ### Workflow 2: Cross-Platform Sync You built iOS deep linking but need Android parity: ```text You: "I have Universal Links on iOS for myapp.com. Set up the Android equivalent with App Links." Cursor (with @redirectly): - Reads your existing iOS configuration - Creates Android App Links with matching paths - Generates assetlinks.json - Ensures both platforms handle the same URLs Result: Consistent deep linking across platforms ``` ### Workflow 3: Environment-Specific Setup You need deep links for dev, staging, and production: ```text You: "Set up deep linking for dev.myapp.com, staging.myapp.com, and myapp.com with proper bundle IDs." Cursor (with @redirectly): - Creates separate configurations for each environment - Uses correct bundle IDs per environment - Sets up routing logic to handle all domains - Provides deployment guidance Result: Seamless testing workflow without manual config ``` ## The Bigger Picture: Beyond Deep Linking Deep linking is just the beginning. The same principle applies to authentication setup, database migrations, API integration, testing, deployment—any task that requires platform knowledge and careful implementation. AI editors with MCP servers represent a fundamental shift in how developers work. We're moving from "looking for answers" to "having a knowledgeable assistant who understands your project." This is the essence of "vibe coding"—development that flows naturally because the tool isn't getting in the way. It's understanding your intent, having the knowledge to execute it correctly, and doing the work while you focus on the next problem. ### The Next Wave As more services and frameworks publish MCP servers, this experience will become standard. Your editor will understand your entire tech stack—not as generic code generator, but as a knowledgeable collaborator. The developers who embraced this workflow early will ship features 10x faster. That's not hyperbole—it's the compounding effect of eliminating context switches, reducing research time, and automating away manual configuration. ## Related - [Set Up Deep Linking with Cursor + MCP](https://redirectly.app/blog/cursor-mcp-deep-linking.md) - [Best Firebase Dynamic Links Alternative](https://redirectly.app/blog/best-firebase-dynamic-links-alternative-2026.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Redirectly MCP Server: Use AI Editors to Build Deep Links > Leverage AI editors to build deep links with Redirectly's MCP server. Setup guides for Cursor, Claude Desktop, VS Code, and more for vibe coding. - Canonical: https://redirectly.app/blog/redirectly-mcp-server-ai-editors - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-25 - Author: Redirectly - Category: AI Tools · 8 min read Leverage AI assistants to build, generate, and manage deep links directly from your editor ## What is MCP and Why It Matters The Model Context Protocol (MCP) is a new standard that enables AI models to connect with tools, services, and data sources. MCP transforms AI editors from simple code assistants into powerful development platforms where AI can directly access external tools without leaving your editor. Before MCP, if you wanted AI to help with deep linking, you'd need to manually copy API calls, run commands separately, or constantly switch between tools. Now, with MCP servers, AI editors can: - Generate deep links with a simple natural language prompt - Validate AASA and assetlinks.json configurations - Create referral and marketing campaign links - Integrate link generation into your development workflow This is "vibe coding"—building software by conversing with AI while having access to real tools that execute actual work. ## Redirectly is the First Deep Linking MCP Server Redirectly's MCP server is the first tool of its kind for deep linking. While other platforms require navigating dashboards or APIs, Redirectly brings deep linking into your AI editor where you're already writing code. With Redirectly's MCP server, you can: - **Generate Referral Links** — Ask your AI editor to create a referral link for a specific user. It generates the link, returns the code, and you can immediately test it. - **Create Marketing Campaign Links** — Generate campaign-specific deep links with custom parameters. Ideal for promotional emails, social media, and ads. - **Validate Configurations** — Check your AASA and assetlinks.json files without leaving your editor. - **Integrate with Development** — Generate links as part of your coding workflow, testing, or documentation. ## Getting Started with Cursor (Most Popular AI Editor) Cursor is the most popular AI-powered code editor. Here's how to set up Redirectly's MCP server: ### 1. Install Redirectly MCP Server First, install the Redirectly MCP server globally via npm: ```bash npm install -g @redirectly/mcp-server ``` ### 2. Configure Cursor Settings Open Cursor settings (Cmd/Ctrl + Shift + P, then "Settings") and add the MCP configuration: ```json { "mcpServers": { "redirectly": { "command": "redirectly-mcp-server", "args": ["--api-key", "your_api_key_here"] } } } ``` Replace "your_api_key_here" with your Redirectly API key (get one at redirectly.app). ### 3. Restart Cursor Close and reopen Cursor. The MCP server is now connected and ready to use. ## Example Session: Building a Referral Deep Link with AI Here's how a typical vibe coding session looks with Redirectly MCP: **You (in Cursor):** "Create a referral deep link for user john@example.com with the code JOHN_FRIEND_2024" **Cursor with Redirectly MCP:** ```json { "success": true, "link": "https://example.com/r/john_friend_2024", "customCode": "JOHN_FRIEND_2024", "deepLinks": { "iOS": "myapp://referral?code=JOHN_FRIEND_2024", "android": "myapp://referral?code=JOHN_FRIEND_2024" } } ``` **You (continuing):** "Now create a marketing campaign link for our new product launch. Track it with campaign=product_launch_march_2024" **Cursor generates and inserts code:** ```javascript // Marketing campaign deep link const productLaunchLink = { url: "https://example.com/launch", ios: "myapp://launch?campaign=product_launch", android: "myapp://launch?campaign=product_launch" }; export const campaignLinks = { email: productLaunchLink.url, social: productLaunchLink.url }; ``` The entire process—generating deep links, getting the code, and integrating it into your project—happens without leaving Cursor. This is the power of MCP-enabled development. ## Supported AI Editors Redirectly's MCP server works with all major AI-powered editors: - **Cursor** — Most popular AI editor with Claude integration - **Claude Desktop** — Anthropic's native desktop application - **Windsurf** — AI-native IDE built for flow - **VS Code + Cody** — VS Code with Sourcegraph's AI assistant - **Zed** — High-performance code editor with AI features - **Continue.dev** — Open-source AI code assistant Setup instructions are similar across all editors. Refer to the specific editor's MCP documentation for detailed configuration steps. ## MCP Server Capabilities Redirectly's MCP server exposes these tools to your AI assistant: - **generate_link** — Create a deep link with custom parameters and metadata - **create_referral_link** — Generate referral links with custom codes and user associations - **create_campaign_link** — Create marketing campaign deep links with campaign tracking - **validate_aasa** — Validate Apple App Site Association configurations - **validate_assetlinks** — Validate Android assetlinks.json configurations - **get_link_analytics** — Retrieve analytics and usage data for your deep links ## Real-World Use Cases Here's how teams use Redirectly's MCP server: - **Onboarding Flows** — Generate verification and password reset links during user onboarding. AI helps create contextual URLs specific to each onboarding step. - **Marketing Campaigns** — Create campaign-specific deep links for email, social media, and paid ads. Track which channels drive installs and engagement. - **Referral Programs** — Generate unique referral codes and deep links for each user. Automate link creation as users opt into referral programs. - **QA and Testing** — Quickly generate test deep links with various parameters to verify your app's deep linking implementation. - **Configuration Validation** — Check AASA and assetlinks.json files while developing, catching issues before deployment. ## Advanced Workflow: Vibe Coding a Referral System Here's an advanced example of vibe coding with Redirectly MCP: ```plaintext You: "Build a referral system with deep links for user alice@example.com" Cursor: - Calls create_referral_link API - Returns referral link and deep link formats - Inserts working code into your file You: "Validate our AASA configuration" Cursor: - Checks AASA file - Reports issues and fixes - Updates configuration You: "Create tests for 10 referral links" Cursor: - Generates test data - Creates test assertions - Ready to run ``` ## Getting Your API Key To use Redirectly's MCP server, you need an API key: 1. **Sign Up or Log In** — Visit redirectly.app and create an account or log in. 2. **Navigate to API Keys** — Go to Settings > API Keys in your dashboard. 3. **Create a New Key** — Click "Create API Key" and give it a name (e.g., "MCP Server"). 4. **Copy and Store Securely** — Copy the key and store it securely. You'll use it in your editor configuration. **Security Note:** Never commit your API key to version control. Use environment variables or secure secret management. ## Learn More About Redirectly Explore more about Redirectly's platform and capabilities: - [Redirectly MCP Platform](https://redirectly.app/mcp.md) — Full documentation and technical details about MCP integration. - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) — Comprehensive guide to deep linking fundamentals and best practices. - [Deferred Deep Linking in Flutter](https://redirectly.app/flutter-deferred-deep-linking.md) — Implement deferred deep linking in your Flutter apps with Redirectly. ## Summary Redirectly's MCP server brings deep linking into your AI editor, enabling vibe coding workflows where AI assists with real deep linking tasks. You can: - Generate referral and campaign deep links by asking your AI editor - Validate AASA and assetlinks.json configurations without context switching - Integrate deep linking into your development workflow seamlessly - Use with all major AI editors (Cursor, Claude Desktop, VS Code, Zed, etc.) - Build faster with AI assistance on deep linking tasks **Ready to try MCP-powered deep linking?** Install Redirectly's MCP server and start vibe coding your next deep link feature. [Explore Redirectly MCP](https://redirectly.app/mcp.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Set Up Deep Linking with Cursor + MCP Server > A complete walkthrough to install the MCP server, configure Cursor, ask it to set up deep links, and verify results with AI-assisted automation. - Canonical: https://redirectly.app/blog/cursor-mcp-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2026-03-07 - Author: Dmytro Vyrych - Category: AI Tools · 10 min read Harness the power of AI-native development. Use Cursor with Redirectly's MCP server to automate and streamline your deep linking setup in minutes. ## What is MCP & Why Use It? **Model Context Protocol:** MCP is an open protocol that connects AI systems like Cursor to external tools and services. It enables AI editors to understand your codebase, access APIs, and execute tasks with precise context—transforming them from general-purpose assistants into specialized tools for your specific workflow. **Benefits for Deep Linking:** - Automate repetitive configuration - AI understands your project structure - Generate platform-specific code - Real-time verification & feedback - Eliminate manual errors - 10x faster setup time **AI-Native Workflow:** - No context switching between tools - Ask natural language questions - Cursor handles all implementation - Multi-file edits in seconds - Instant code generation - Built-in testing & verification ## Install the Redirectly MCP Server ### Step 1: Install via npm The Redirectly MCP server is available on npm. Install it globally or locally in your project: ```bash npm install -g @redirectly/mcp-server ``` **Tip:** Global installation makes the server available from any directory. ### Step 2: Verify Installation Verify that the MCP server is installed correctly: ```bash redirectly-mcp --version ``` You should see a version number like `1.0.0` ## Configure Cursor ### Step 1: Open Cursor Settings Open Cursor and access the MCP configuration: 1. Press `Cmd/Ctrl + K` to open Command Palette 2. Search for "MCP" or "Model Context Protocol" 3. Select "Cursor Settings: MCP" 4. Open the JSON configuration file ### Step 2: Add Redirectly MCP Server Add the Redirectly MCP server to your Cursor configuration: ```json { "mcpServers": { "redirectly": { "command": "redirectly-mcp", "args": [], "env": { "REDIRECTLY_API_KEY": "your-api-key-here" } } } } ``` ### Step 3: Set Your API Key Get your API key from Redirectly and add it to the configuration. Visit [your dashboard](https://dashboard.redirectly.app/dashboard) to find your API key. **Security:** Never commit your API key to version control. Use environment variables or a .env.local file instead. ### Step 4: Restart Cursor Close and reopen Cursor to apply the new MCP server configuration. ```text Cmd/Ctrl + Q to quit Cursor Reopen Cursor from your applications ``` ## Ask Cursor to Set Up Deep Links ### Natural Language Prompts Now you can ask Cursor to handle deep linking setup directly. Use natural language to describe what you need: ### Example 1: Flutter iOS Setup Ask Cursor to set up Universal Links for iOS: ```text @redirectly Set up Universal Links for iOS in my Flutter app. My domain is myapp.com. Configure Associated Domains, create the AASA file, and add the deep link handler to my main.dart. ``` ### Example 2: Android App Links Setup Ask Cursor to configure Android App Links: ```text @redirectly Configure App Links for Android in my React Native app. Domain: myapp.com App packages: com.myapp.dev, com.myapp.prod Add all required manifest configurations and deep link handlers. ``` ### Example 3: Full Stack Setup Ask for complete deep linking implementation across platforms: ```text @redirectly Set up complete deep linking for my app: - iOS: Universal Links for myapp.com and www.myapp.com - Android: App Links for the same domains - Create AASA file and assetlinks.json - Configure routing for /products/:id, /users/:id, and /share/:code paths - Add handlers to my app startup code ``` ### How It Works When you use the @redirectly mention, Cursor: - Analyzes your project structure and configuration - Accesses the Redirectly MCP server for best practices - Generates platform-specific configuration files - Creates or updates all necessary source code - Provides testing guidance and verification steps ## Verify Your Results ### Check Generated Files After Cursor completes the setup, verify that all necessary files have been created: ```bash # iOS ls ios/Runner.xcodeproj/ # Android ls android/app/src/ # Web (AASA file) ls web/.well-known/apple-app-site-association ``` ### Test on Device Build and test your app on real devices to verify deep linking works: - **iOS:** Run `flutter run` on a real device and test with deep links in Notes or Safari - **Android:** Test with adb command to trigger deep links: `adb shell am start -a android.intent.action.VIEW -d "myapp.com/products/123"` ### Use Redirectly's Verification Tools Validate your configuration using the online tools: - [AASA Validator](https://redirectly.app/aasa-validator.md) - [assetlinks.json Validator](https://redirectly.app/assetlinks-validator.md) ## Troubleshooting ### MCP Server Not Connecting **Issue:** Cursor doesn't recognize the @redirectly mention **Solution:** - Verify the MCP server installed: `redirectly-mcp --version` - Check MCP configuration in Cursor settings - Restart Cursor completely - Check that API key is set correctly ### Generated Code Has Errors **Issue:** Cursor generated code that doesn't compile **Solution:** - Provide more context in your initial request - Specify your project structure clearly - Ask Cursor to verify the generated code - Provide error messages for Cursor to fix ### API Key Issues **Issue:** "Invalid API key" or authentication errors **Solution:** - Visit [your dashboard](https://dashboard.redirectly.app/dashboard) to get a fresh API key - Update your Cursor MCP configuration - Ensure no extra spaces or quotes in the API key - Restart Cursor after updating ## Get Started with MCP Today Transform your deep linking workflow with AI-native development. Set up complex routing in minutes instead of hours. Learn more on the [MCP page](https://redirectly.app/mcp.md) or get your API key at https://dashboard.redirectly.app/. ## Related - [Vibe Coding with Deep Links](https://redirectly.app/blog/vibe-coding-deep-links.md) — Explore AI-native development workflows and how MCP servers revolutionize developer experience. - [Flutter Universal Links Setup](https://redirectly.app/blog/flutter-universal-links-ios-setup.md) — Learn the manual approach to deep linking configuration for complete understanding. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Firebase Dynamic Links Shutdown: Timeline & What To Do > Firebase Dynamic Links shut down in August 2025. Learn the timeline, what broke in your apps, and discover better alternatives like Redirectly. - Canonical: https://redirectly.app/blog/firebase-dynamic-links-shutdown - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-12-15 - Author: Dmytro Vyrych - Category: Migration · 8 min read Firebase Dynamic Links (FDL) are being retired in August 2025. Learn what broke for your apps, the complete timeline, and discover better alternatives to migrate your deep linking infrastructure. > **Urgent: Firebase Dynamic Links Sunset August 5, 2025** — If you're still using Firebase Dynamic Links in production, you need to migrate immediately. Links will stop working after the sunset date. ## Table of Contents - 1. Deprecation Timeline - 2. What Broke for Apps - 3. Business Impact - 4. Available Alternatives - 5. Why Redirectly Was Built - 6. Your Migration Path ## Firebase Dynamic Links Deprecation Timeline ### May 2023: Announcement Google announced Firebase Dynamic Links deprecation, giving developers 2 years to migrate. ### December 2023: Warnings Begin Apps using FDL started receiving deprecation warnings in Firebase console and SDKs. ### August 5, 2025: Sunset Date Firebase Dynamic Links service stops working. All links become inactive. ### Post-August 2025: Zero Support No further updates or support. Service completely removed from Firebase platform. ## What Broke for Apps Using FDL ### Deferred Deep Linking Apps relying on FDL's deferred deep linking capability lost the ability to track installs and route users to specific content on first launch. ### Attribution Loss Firebase Dynamic Links provided install attribution data. Apps lost visibility into how installs were generated. ### App Sharing Features In-app sharing, referral programs, and affiliate links built on FDL stopped working after the sunset date. ### Cross-Platform Tracking FDL's ability to track users across web and mobile was lost, breaking integrated marketing funnels. ### Real-World Impact Thousands of apps in production were affected when Firebase Dynamic Links shut down. Common scenarios that broke: - Users sharing product links couldn't route to content on app install - Referral programs stopped tracking reward attribution - Marketing campaigns lost install source tracking - Affiliate networks couldn't attribute commissions ## Business Impact of FDL Shutdown ### User Engagement Drop Apps depending on deep linking saw a significant drop in user engagement when links stopped working. Users who couldn't access shared content directly were more likely to churn. ### Revenue Loss Referral programs, affiliate networks, and marketing campaigns relying on install attribution lost the ability to track ROI and compensate partners, resulting in direct revenue impact. ### Operational Disruption Engineering teams had to quickly rebuild deep linking infrastructure with minimal notice, diverting resources from feature development to fire-fighting. ## Available Deep Linking Alternatives ### Google Play Referrer API Android's built-in install attribution. Limited to Android, no deferred deep linking. - **Pros:** Free, native Android integration - **Cons:** Android-only, basic attribution only ### Universal Links & App Links Apple and Google's native deep linking protocols. No attribution or deferred linking. - **Pros:** Native, SEO-friendly - **Cons:** No attribution, requires domain control ### Branch.io Full-featured deep linking platform with attribution and mobile measurement. - **Pros:** Feature-rich, well-established - **Cons:** Expensive, complex SDK integration ### Adjust / AppsFlyer Mobile measurement platforms with deep linking and attribution. - **Pros:** Industry standard, comprehensive analytics - **Cons:** High cost, bloated SDKs ### Redirectly Modern alternative built specifically for developers moving from Firebase. - **Pros:** Lightweight, easy migration, affordable - **Cons:** Newer platform (but that's why it was designed right) ## Why Redirectly Was Built > **The Firebase Dynamic Links Gap:** When Firebase Dynamic Links shut down, there was no ideal replacement. Existing alternatives were either too expensive, too complex, or missing critical features that developers relied on. Redirectly was built to fill this gap. ### Purpose-Built for FDL Migration Redirectly's architecture is optimized for apps migrating from Firebase Dynamic Links, with familiar concepts and easy SDKs. ### Developer-Friendly Minimal SDK footprint, clean documentation, and SDK support for React Native, Flutter, iOS, and Android without bloat. ### Transparent Pricing Straightforward pricing without surprise fees. You only pay for what you use, not for a bloated platform. ## Your Migration Path ### 1. Audit Your FDL Usage Identify all places in your codebase where Firebase Dynamic Links are used. Check analytics for usage patterns. ### 2. Choose a Replacement Evaluate alternatives based on your features (deferred linking, attribution, etc.) and budget. Redirectly is optimized for this migration path. ### 3. Integrate New Solution Install and configure the new SDK. Most migrations take a few hours with Redirectly's straightforward APIs. ### 4. Remove FDL Code Once verified, remove Firebase Dynamic Links SDK and code from your application. ### 5. Deploy & Monitor Deploy the updated code and monitor deep linking performance. Most teams see zero downtime with proper staging. ## Related Articles - [Firebase Dynamic Links vs Redirectly](https://redirectly.app/blog/firebase-dynamic-links-vs-redirectly.md) — Feature-by-feature comparison of Firebase Dynamic Links and Redirectly with pricing analysis. - [Migrate from Firebase Dynamic Links in React Native](https://redirectly.app/blog/react-native-firebase-dynamic-links-migration.md) — Step-by-step React Native migration guide with TypeScript code examples. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # UTM Parameters for Mobile Apps: Complete Guide > Learn how to structure UTM parameters for deep links, preserve them through install, and read UTM data in Flutter and React Native apps. - Canonical: https://redirectly.app/blog/utm-parameters-mobile-apps - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-12-05 - Author: Dmytro Vyrych - Category: Marketing · 8 min read Learn how to effectively use UTM parameters in mobile apps for campaign tracking. Includes best practices, implementation examples in Flutter and React Native, and how to preserve UTMs through install. ## Table of Contents 1. What are UTM Parameters? 2. UTM Structure & Parameters 3. UTMs in Mobile Deep Links 4. Preserving UTMs Through Install 5. Flutter Implementation 6. React Native Implementation 7. Best Practices ## What are UTM Parameters? **Definition:** UTM parameters (Urchin Tracking Module) are URL parameters that you add to links to track the source, medium, and campaign that drove traffic to your app. They're a standard way to measure marketing effectiveness. ### Why They Matter - Track campaign performance - Measure marketing ROI - Identify top traffic sources - Optimize budget allocation - Monitor ad effectiveness - Standard across analytics ### Web vs Mobile - **Web:** UTMs in URL query string - **Mobile:** UTMs must be preserved in deep links - **Challenge:** Re-installs lose UTM data - **Solution:** Deferred deep linking services - **Analytics:** Same UTM structure applies ## UTM Parameters Structure ### 5 Standard UTM Parameters - **utm_source** — The referrer: google, facebook, twitter, newsletter, etc. Example: `utm_source=google` - **utm_medium** — The marketing medium: cpc, social, email, organic, etc. Example: `utm_medium=cpc` - **utm_campaign** — The campaign name: summer_sale, q1_promotion, etc. Example: `utm_campaign=summer_sale` - **utm_content** — Optional: differentiates similar content. Useful for A/B testing. Example: `utm_content=banner_v2` - **utm_term** — Optional: keyword for paid search campaigns. Example: `utm_term=mobile+app` ### Example: Complete UTM URL ```text https://redirectly.app/product/123?utm_source=facebook&utm_medium=social&utm_campaign=summer_sale&utm_content=carousel_ad ``` This URL tells you: Traffic came from Facebook (source), via social media (medium), for the summer sale campaign (campaign), using a carousel ad (content). ## UTMs in Mobile Deep Links ### Mobile Deep Link Format Mobile deep links can include UTM parameters. The format depends on your deep linking scheme: ```text // Custom scheme myapp://product/123?utm_source=google&utm_medium=cpc&utm_campaign=summer // Universal/App Links https://myapp.com/product/123?utm_source=google&utm_medium=cpc&utm_campaign=summer // Deep link service (Redirectly) https://redirectly.app/link/abc123?utm_source=google&utm_medium=cpc&utm_campaign=summer ``` ### UTM Best Practices for Mobile - **Use lowercase:** Always use lowercase for parameter values for consistency - **Use underscores:** Use underscores instead of spaces (e.g., summer_sale not summer sale) - **Keep it short:** Shorter URLs are less prone to being truncated by platforms - **Use URL encoding:** Properly encode special characters (%20 for space, etc.) - **Consistent naming:** Use the same source/medium values across campaigns ## Preserving UTMs Through Install ### The Challenge When a user clicks a deep link and installs your app for the first time, the URL parameters are lost during the install process. This is called "deferred linking" and requires special handling to preserve UTM data. ### Solution: Deferred Deep Linking Deferred deep linking services capture UTM data before app install and deliver it to your app after installation: 1. **User clicks deep link with UTMs** — `https://redirectly.app/product/123?utm_source=facebook` 2. **Service captures UTM data and redirects to app store** — UTMs stored on Redirectly servers 3. **User installs app** — App opens for first time 4. **App retrieves stored UTM data** — UTMs delivered to app and route user accordingly ## Flutter Implementation ### Reading UTM Parameters from Deep Links ```dart import 'package:flutter/material.dart'; import 'package:uni_links/uni_links.dart'; import 'dart:async'; class UTMTracker { static Future> extractUTMParameters(Uri uri) async { final params = {}; // Extract UTM parameters from query string final source = uri.queryParameters['utm_source'] ?? ''; final medium = uri.queryParameters['utm_medium'] ?? ''; final campaign = uri.queryParameters['utm_campaign'] ?? ''; final content = uri.queryParameters['utm_content'] ?? ''; final term = uri.queryParameters['utm_term'] ?? ''; if (source.isNotEmpty) params['utm_source'] = source; if (medium.isNotEmpty) params['utm_medium'] = medium; if (campaign.isNotEmpty) params['utm_campaign'] = campaign; if (content.isNotEmpty) params['utm_content'] = content; if (term.isNotEmpty) params['utm_term'] = term; return params; } static Future trackUTMs(Uri uri) async { final utms = await extractUTMParameters(uri); if (utms.isNotEmpty) { print('UTM Parameters: $utms'); // Send to analytics _sendToAnalytics(utms); } } static void _sendToAnalytics(Map utms) { // Example: Send to Firebase Analytics or your analytics service // analytics.logEvent( // name: 'app_opened_via_utm', // parameters: utms, // ); } } class DeepLinkHandler { static Future initialize() async { // Handle initial deep link (cold start) final String? initialLink = await getInitialLink(); if (initialLink != null) { final uri = Uri.parse(initialLink); await UTMTracker.trackUTMs(uri); } // Listen for deep links when app is running linkStream.listen( (String link) async { final uri = Uri.parse(link); await UTMTracker.trackUTMs(uri); }, onError: (err) { debugPrint('Deep link error: $err'); }, ); } } ``` ### Using Redirectly for UTM Preservation Redirectly automatically preserves UTM parameters through install: ```dart import 'package:redirectly_flutter/redirectly_flutter.dart'; class MyApp extends StatefulWidget { @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); _initializeRedirectly(); } Future _initializeRedirectly() async { // Initialize Redirectly await Redirectly.init(apiKey: 'your-api-key'); // Listen for deep links with UTM parameters Redirectly.onDeepLink((deepLink) { final path = deepLink.path; final campaign = deepLink.campaign; // deepLink.data contains all UTM parameters final utmSource = deepLink.data['utm_source']; final utmMedium = deepLink.data['utm_medium']; final utmCampaign = deepLink.data['utm_campaign']; print('Path: $path'); print('Campaign: $campaign'); print('UTM Source: $utmSource'); print('UTM Medium: $utmMedium'); print('UTM Campaign: $utmCampaign'); // Route user and track analytics _handleDeepLink(deepLink); }); } void _handleDeepLink(RedirectlyDeepLink deepLink) { // Route to correct screen based on path // Log attribution data for analytics } @override Widget build(BuildContext context) { return MaterialApp( title: 'My App', home: const HomeScreen(), ); } } ``` ## React Native Implementation ### Reading UTM Parameters with Redirectly ```typescript import { useEffect } from 'react'; import { useNavigation } from '@react-navigation/native'; import { Redirectly } from '@redirectly/react-native'; interface UTMData { utm_source?: string; utm_medium?: string; utm_campaign?: string; utm_content?: string; utm_term?: string; } export const useUTMTracking = () => { const navigation = useNavigation(); useEffect(() => { // Handle initial deep link on app launch Redirectly.getInitialLink().then((deepLink) => { if (deepLink) { handleDeepLinkWithUTM(deepLink); } }); // Listen for deep links when app is running const unsubscribe = Redirectly.onDeepLink((deepLink) => { handleDeepLinkWithUTM(deepLink); }); return unsubscribe; }, []); const handleDeepLinkWithUTM = (deepLink: DeepLinkData) => { const { path, campaign, data } = deepLink; // Extract UTM parameters const utm: UTMData = { utm_source: data?.utm_source, utm_medium: data?.utm_medium, utm_campaign: data?.utm_campaign, utm_content: data?.utm_content, utm_term: data?.utm_term, }; // Log UTM data for analytics console.log('Deep Link UTM Data:', utm); // Send to analytics service logToAnalytics(utm); // Route to correct screen if (path.startsWith('/product/')) { const productId = path.split('/')[2]; navigation.navigate('ProductScreen', { productId, utm }); } else if (path.startsWith('/promo/')) { navigation.navigate('PromoScreen', { utm }); } else { navigation.navigate('Home', { utm }); } }; return null; }; const logToAnalytics = (utm: UTMData) => { // Send to your analytics service // Example: Firebase Analytics, Amplitude, Mixpanel if (utm.utm_source || utm.utm_campaign) { console.log('Logging UTM attribution:', utm); // analytics.logEvent('deep_link_opened', utm); } }; ``` ## UTM Best Practices ### 1. Standardize Your Naming Create a company-wide naming convention for UTM values. Use consistent source and medium names across all campaigns. ### 2. Use URL Builders Use Google Analytics URL Builder or similar tools to generate UTM links. This reduces typos and ensures consistency. ### 3. Track in Analytics Send UTM data to your analytics service on app open. This creates a complete view of user acquisition and engagement. ### 4. Use Deferred Deep Linking Implement deferred deep linking to preserve UTM parameters through app installs. This ensures attribution works even for new users. ### 5. Monitor UTM Data Quality Regularly review your UTM data for completeness and accuracy. Check for missing or inconsistent values that might indicate problems. ## Related - [Mobile Install Attribution: Complete Guide](https://redirectly.app/blog/mobile-install-attribution-guide.md) - [Deferred Deep Linking Guide](https://redirectly.app/deferred-deep-linking.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Branch vs AppsFlyer vs Adjust: Deep Linking Comparison > Compare Branch, AppsFlyer, and Adjust for deep linking and attribution. See pricing, SDK support, deferred deep linking, and a lightweight alternative. - Canonical: https://redirectly.app/blog/branch-vs-appsflyer-vs-adjust - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2025-03-07 - Author: Dmytro Vyrych - Category: Comparison · 8 min read Choose the right deep linking platform for your app. Compare Branch, AppsFlyer, and Adjust across pricing, SDK support, deferred deep linking capabilities, attribution, privacy, and ease of setup. Discover why Redirectly offers a lightweight alternative. ## Table of Contents 1. Platform Overview 2. Feature Comparison Table 3. Pricing Analysis 4. SDK Support: Flutter & React Native 5. Deferred Deep Linking 6. Attribution & Analytics 7. Privacy & Compliance 8. Redirectly: Lightweight Alternative ## Platform Overview ### Branch Mobile measurement and deep linking platform focused on app growth and user acquisition. - Founded: 2011 - HQ: Palo Alto, CA - Primary: Growth Attribution - Best for: Startups, App Growth ### AppsFlyer Comprehensive mobile marketing platform with deep linking, attribution, and analytics. - Founded: 2011 - HQ: Tel Aviv, Israel - Primary: Attribution + Analytics - Best for: Enterprise Apps ### Adjust Mobile measurement platform with privacy-first approach and flexible deep linking. - Founded: 2012 - HQ: Berlin, Germany - Primary: Privacy-First Analytics - Best for: EU/Privacy-Conscious Apps ## Feature Comparison Table | Feature | Branch | AppsFlyer | Adjust | Redirectly | | --- | --- | --- | --- | --- | | Basic Deep Linking | Yes | Yes | Yes | Yes | | Deferred Deep Linking | Yes | Yes | Yes | Yes | | Universal Links/App Links | Yes | Yes | Yes | Yes | | Flutter SDK | Yes | Yes | Yes | Yes | | React Native SDK | Yes | Yes | Yes | Yes | | Attribution Tracking | Yes | Yes | Yes | No | | Event Analytics | Yes | Yes | Yes | No | | Privacy-First | No | No | Yes | Yes | | No Analytics Bloat | No | No | No | Yes | | Open Source Option | No | No | No | Yes | ## Pricing Analysis ### Branch - **Free Tier:** Up to 10,000 clicks/month - **Pro:** Custom pricing based on volume - **Enterprise:** Dedicated support + custom features - Typical cost: $500-$5,000+/month depending on scale ### AppsFlyer - **Starter:** Limited analytics, $0-500/month - **Growth:** Enhanced features, $500-2,000/month - **Enterprise:** Full platform, custom pricing - Typical cost: $1,000-$10,000+/month for active apps ### Adjust - **Free:** Basic analytics - **Growth:** Advanced features, $250-1,000/month - **Enterprise:** Custom implementation - Typical cost: $500-$5,000+/month for enterprise ### Redirectly Pricing - **Free Plan:** Full deep linking functionality, no limits - **Pay-as-you-grow:** Simple, transparent pricing - **No analytics overhead:** Only pay for what you need - Focus: Deep linking only (no bloat). Starts at $0/month ## SDK Support: Flutter & React Native ### Flutter SDK | Platform | Support | | --- | --- | | Branch | Complete | | AppsFlyer | Complete | | Adjust | Complete | | Redirectly | Complete | ### React Native SDK | Platform | Support | | --- | --- | | Branch | Complete | | AppsFlyer | Complete | | Adjust | Complete | | Redirectly | Complete | **Note:** All major platforms support both Flutter and React Native. The difference is in SDK complexity, maintenance frequency, and documentation quality. ## Deferred Deep Linking Capabilities ### How They Compare **Branch** Industry leader in deferred deep linking. Stores click data and delivers on first app open. Excellent for user acquisition campaigns. **AppsFlyer** Robust deferred deep linking tied to attribution. Good for campaigns but requires analytics integration. **Adjust** Privacy-focused deferred deep linking. Works without full attribution tracking. Good balance for privacy-conscious apps. **Redirectly** Lightweight deferred deep linking without analytics overhead. Perfect for apps that only need routing, not attribution. ## Attribution & Analytics Features ### Platform Strengths **Branch** - Probabilistic + deterministic attribution - Real-time analytics dashboards - Campaign ROI tracking - Cohort analysis **AppsFlyer** - Most comprehensive analytics - Advanced attribution modeling - Custom event tracking - LTV predictions **Adjust** - Privacy-preserving attribution - SKAdNetwork integration - User data protection - GDPR-friendly analytics ## Privacy & Compliance Considerations ### GDPR & Data Privacy **Branch & AppsFlyer** GDPR compliant but collect extensive user data. Requires explicit consent in EU. **Adjust** Privacy-first approach. Fewer tracking requirements. Better for EU-first products. **Redirectly** Minimal data collection. No user tracking. Excellent for privacy-focused apps. ### iOS 14+ Tracking Changes All platforms have adapted to Apple's privacy changes. The real difference: - **Branch:** Uses SKAdNetwork + fingerprinting - **AppsFlyer:** Comprehensive SKAdNetwork support - **Adjust:** Privacy-first SKAdNetwork approach - **Redirectly:** No tracking needed for deep linking ## Redirectly: A Lightweight Alternative ### When to Choose Redirectly If your primary need is deep linking without the overhead of attribution and analytics platforms, Redirectly is an excellent choice. - **Cost-Effective:** Free deep linking foundation, no hidden analytics costs - **Privacy First:** Minimal data collection, user-focused design - **Simple Integration:** Focused API for deep linking only - **No Lock-in:** Easy to implement and remove if needed - **Full SDK Support:** Complete Flutter and React Native implementations ### Comparison Summary **Choose Branch or AppsFlyer if:** You need comprehensive attribution, detailed user journey tracking, and don't mind the complexity and cost of a full marketing platform. **Choose Adjust if:** Privacy compliance is critical, you're EU-focused, and you want attribution without excessive tracking. **Choose Redirectly if:** You only need reliable deep linking, want to minimize costs, prefer simplicity, and plan to use separate analytics tools. ## Key Takeaways 1. Branch excels at deferred deep linking and app growth campaigns 2. AppsFlyer offers the most comprehensive analytics but at enterprise pricing 3. Adjust is best for privacy-focused apps and EU compliance 4. Redirectly provides lightweight deep linking without analytics overhead 5. All platforms support Flutter and React Native equally well ## Related Articles - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) — Discover why Redirectly is a better fit for lightweight deep linking without attribution bloat. - [Adjust Alternative](https://redirectly.app/adjust-alternative.md) — Privacy-first deep linking that's even simpler than Adjust, without unnecessary analytics. - [Complete Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) — Learn all about deep linking strategies, implementation patterns, and best practices. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Mobile Install Attribution: Complete Guide > A complete guide to mobile install attribution covering attribution models, Google Play Referrer, iOS SKAN, and key privacy considerations. - Canonical: https://redirectly.app/blog/mobile-install-attribution-guide - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-12-10 - Author: Dmytro Vyrych - Category: Attribution · 14 min read Learn how to properly track and attribute mobile app installs. Covers attribution models, platform-specific APIs, privacy considerations, and best practices. ## What is Mobile Install Attribution? **Definition:** Mobile install attribution is the process of determining which marketing channel, campaign, or user action led to an app installation. It tracks the source of installs and attributes them to specific marketing efforts. **Why It Matters:** - Measure marketing ROI - Identify best channels - Optimize spend allocation - Track campaign performance - Understand user sources **Key Metrics:** - Install source - Campaign name - Ad network - Attribution window - Cost per install **Challenges:** - Privacy regulations - Device fingerprinting - Cross-device tracking - Attribution windows - Data accuracy ## Attribution Models Explained Different attribution models assign credit to marketing touchpoints in different ways. Here are the most common: ### 1. First-Touch Attribution Credits the first marketing touchpoint a user encounters. All install credit goes to the first interaction. **Example:** User sees Facebook ad → clicks → waits 3 days → sees Google ad → installs app. **Attribution:** Facebook gets 100% credit. - **Best for:** Awareness and top-of-funnel campaigns - **Limitation:** Ignores later touchpoints that may have driven conversion ### 2. Last-Touch Attribution Credits the last marketing touchpoint before the install. Most common model in mobile marketing. **Example:** User sees Facebook ad → clicks → waits 3 days → sees Google ad → installs app. **Attribution:** Google gets 100% credit. - **Best for:** Conversion and performance marketing - **Limitation:** Ignores earlier touchpoints that may have influenced decision ### 3. Multi-Touch Attribution Distributes credit across multiple touchpoints. Can use various weights (linear, time-decay, algorithmic). **Example (Linear):** User sees Facebook ad → clicks → waits 3 days → sees Google ad → installs app. **Attribution:** Facebook 50%, Google 50%. - **Best for:** Holistic ROI analysis and comprehensive reporting - **Limitation:** More complex, requires data infrastructure ### 4. Time-Decay Attribution Gives more credit to touchpoints closer to the install. Earlier interactions receive less credit. **Example:** User sees Facebook ad → clicks → waits 3 days → sees Google ad → installs app. **Attribution:** Facebook 30%, Google 70%. - **Best for:** Short conversion windows and retargeting - **Limitation:** May undervalue awareness efforts ## Google Play Referrer API The Google Play Referrer API is Google's official method for tracking installs on Android. It captures referral information when a user installs your app from the Google Play Store. ### How It Works When a user clicks an ad and installs your app, Google Play captures referral information: ```json { "referrer": "utm_source=google_play&utm_medium=cpc&utm_campaign=spring_sale", "referrer_click_timestamp_seconds": 1702000000, "install_begin_timestamp_seconds": 1702000060, "install_timestamp_seconds": 1702000120, "google_play_instant": false } ``` ### Implementation Example ```kotlin import com.android.installreferrer.api.InstallReferrerClient import com.android.installreferrer.api.InstallReferrerStateListener class AttributionHelper : InstallReferrerStateListener { private lateinit var referrerClient: InstallReferrerClient fun getInstallReferrer(context: Context) { referrerClient = InstallReferrerClient.newBuilder(context).build() referrerClient.startConnection(this) } override fun onInstallReferrerSetupFinished(responseCode: Int) { when (responseCode) { InstallReferrerClient.InstallReferrerResponse.OK -> { val response = referrerClient.installReferrer val referrerUrl = response.installReferrer val clickTime = response.referrerClickTimestampSeconds val installTime = response.installBeginTimestampSeconds logAttribution(referrerUrl, clickTime, installTime) } InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> { // API not available } InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> { // Service not available } } referrerClient.endConnection() } override fun onInstallReferrerServiceDisconnected() { // Try again later } private fun logAttribution(referrer: String, clickTime: Long, installTime: Long) { // Send to your backend or attribution service } } ``` ### Limitations - **Attribution window:** Limited to 90 days from install - **Organic installs:** No referral data for users who directly visit Play Store - **Side-loaded installs:** Does not work for apps installed outside Play Store - **Device ID access:** Restricted by Play Services policies ## iOS SKAN Attribution SKAdNetwork (SKAN) is Apple's privacy-focused attribution framework introduced with iOS 14.5. It replaces IDFA-based tracking with aggregated, privacy-preserving measurement. ### Key Differences from Traditional Attribution - **No user-level data:** Only aggregated conversion data across users - **Short attribution window:** 0-7 days for iOS 16+, 24 hours for iOS 14-15 - **Limited conversion data:** Only 6 bits of conversion value (64 possible values) - **Delayed reporting:** 24-48 hour delay before data is available ### Implementing SKAN ```swift import StoreKit // Configure conversion values func configureConversionTracking() { // Update conversion value as user engages with app if #available(iOS 15.4, *) { try? updatePostbackConversionValue( conversionValue: 25, // 0-63 value coarseValue: .medium, // low, medium, high lockWindow: false ) } else if #available(iOS 14.5, *) { SKAdNetwork.updateConversionValue(25) } } // Report SKAN postback func reportSKANEvent() { if #available(iOS 15.1, *) { Task { do { try await requestPostbackOverride() } catch { print("SKAN postback override failed: \(error)") } } } } ``` ## Privacy & Tracking Changes ### iOS: App Tracking Transparency (ATT) iOS 14.5+ requires explicit user consent to track activity across apps and websites using IDFA (Identifier for Advertisers). - **Low consent rates:** Industry average is 25-30% users opt-in to tracking - **SKAN requirement:** Advertisers must use SKAN for privacy-compliant attribution - **Contextual alternatives:** Consider contextual attribution and on-device measurement ### Android: Privacy Sandbox Google is phasing out third-party cookies and moving toward privacy-preserving measurement: - **Topics API:** Advertising categories based on user behavior (not individual browsing) - **Aggregation API:** Aggregated reporting instead of individual-level data - **Google Play Referrer:** Still available but limited data in future ### Best Practices for Privacy-Compliant Attribution - Implement transparent privacy policies - Obtain explicit user consent before tracking - Use aggregated data for analytics and reporting - Comply with GDPR, CCPA, and regional privacy laws - Minimize personal data collection ## Implementation Best Practices ### 1. Multi-Platform Strategy Different platforms require different attribution approaches. Implement a comprehensive strategy that works across iOS and Android. ### 2. Server-Side Tracking Send attribution data to your backend for processing and storage, reducing dependency on client-side tracking. ### 3. Use Attribution Services Consider using services like Redirectly that handle platform-specific APIs and provide unified attribution across iOS and Android. ### 4. Monitor Attribution Quality Regularly audit attribution data for accuracy. Check for discrepancies between app-level and platform-level attribution. ## Start Tracking Attribution Today Redirectly makes mobile install attribution simple and privacy-compliant across iOS and Android platforms. Get started at [Redirectly](https://redirectly.app/index.md) or learn about [deferred deep linking](https://redirectly.app/deferred-deep-linking.md). ## Related - [UTM Parameters for Mobile Apps: Complete Guide](https://redirectly.app/blog/utm-parameters-mobile-apps.md) — Learn how to structure and use UTM parameters in mobile deep links for better campaign tracking. - [Deferred Deep Linking Guide](https://redirectly.app/deferred-deep-linking.md) — Complete guide to implementing deferred deep linking for post-install user routing. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Flutter Deep Linking with go_router > Master Flutter deep linking with go_router. Learn route configuration, path parameters, redirect guards, and deferred deep link handling. - Canonical: https://redirectly.app/blog/flutter-deep-linking-go-router - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-20 - Author: Redirectly - Category: Flutter · 8 min read Learn how to implement powerful deep linking in Flutter using go_router. Master route configuration, path parameters, redirect guards, and deferred deep link handling with complete code examples. ## Table of Contents 1. Why go_router for Deep Linking? 2. Getting Started with go_router 3. Configuring Deep Link Routes 4. Path Parameter Extraction 5. Redirect Guards & Auth Checks 6. Deferred Deep Link Handling 7. Full Working Example ## Why go_router for Deep Linking? ### Key Advantages - Declarative route configuration - Built-in deep link support - Path parameter extraction - Redirect guards for auth - Deep link state tracking - Async parameter parsing ### Deep Linking Features - Automatic link parsing - Named route parameters - Query parameter support - Fallback routes - Error handling - State preservation ### go_router vs Manual Handling While uni_links captures deep links, go_router handles the routing logic. Together, they create a complete deep linking solution with declarative route configuration and automatic URL parsing. ## Getting Started with go_router ### Step 1: Add Dependencies Add go_router and uni_links to your pubspec.yaml: ```yaml dependencies: flutter: sdk: flutter go_router: ^13.0.0 uni_links: ^0.9.0 ``` ### Step 2: Install Packages Run flutter pub get: ```bash flutter pub get ``` ## Configuring Deep Link Routes ### Create Router Configuration Set up your GoRouter with deep link routes: ```dart import 'package:go_router/go_router.dart'; import 'package:flutter/material.dart'; final GoRouter router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ // Product route with parameter GoRoute( path: 'product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), // User profile route GoRoute( path: 'user/:userId', builder: (context, state) { final userId = state.pathParameters['userId']!; return UserProfileScreen(userId: userId); }, ), // Referral/share route GoRoute( path: 'share/:referralCode', builder: (context, state) { final referralCode = state.pathParameters['referralCode']!; return ShareScreen(referralCode: referralCode); }, ), ], ), // 404 not found route GoRoute( path: '/404', builder: (context, state) => const NotFoundScreen(), ), ], // Handle unmatched routes errorBuilder: (context, state) => const NotFoundScreen(), ); ``` ### Use Router in Main App Initialize the router in your MaterialApp: ```dart void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'My App', routerConfig: router, theme: ThemeData( primarySwatch: Colors.blue, ), ); } } ``` ## Path Parameter Extraction ### Accessing Path Parameters Extract and use path parameters in your screens: ```dart class ProductScreen extends StatelessWidget { final String productId; const ProductScreen({Key? key, required this.productId}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Product')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Product ID: $productId'), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Load product details _loadProductDetails(productId); }, child: const Text('Load Product'), ), ], ), ), ); } Future _loadProductDetails(String productId) async { debugPrint('Loading product: $productId'); // Fetch from API using productId } } ``` ### Query Parameters Access query parameters from the deep link: ```dart GoRoute( path: 'product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; // Access query parameters final String? source = state.uri.queryParameters['source']; final String? campaign = state.uri.queryParameters['campaign']; return ProductScreen( productId: productId, source: source, campaign: campaign, ); }, ), ``` ## Redirect Guards & Auth Checks ### Implement Auth Guard Protect routes with authentication redirects: ```dart final GoRouter router = GoRouter( redirect: (context, state) { // Check if user is authenticated final isAuthenticated = _isUserAuthenticated(); final isLoggingIn = state.uri.path == '/login'; if (!isAuthenticated && !isLoggingIn) { // Redirect to login, but save the requested path return '/login?from=${state.uri.path}'; } if (isAuthenticated && isLoggingIn) { // Already logged in, redirect to home return '/'; } return null; // No redirect needed }, routes: [ // Your routes here ], ); ``` ### Route-Level Guards Add guards to specific routes using redirect: ```dart GoRoute( path: 'user/:userId', redirect: (context, state) { // Check if user has permission to view final userId = state.pathParameters['userId']!; final currentUserId = _getCurrentUserId(); if (userId != currentUserId && !_isAdmin()) { return '/'; // Redirect to home if no permission } return null; // Allow access }, builder: (context, state) { final userId = state.pathParameters['userId']!; return UserProfileScreen(userId: userId); }, ), ``` ## Deferred Deep Link Handling ### Handle Deferred Links with go_router Implement deferred deep link handling with initial link detection: ```dart import 'package:uni_links/uni_links.dart'; class DeepLinkManager { static Future initializeDeepLinks(GoRouter router) async { // Handle initial deep link (app launched from deep link) final String? initialLink = await getInitialLink(); if (initialLink != null) { await _processDeepLink(initialLink, router); } // Handle deep links while app is running linkStream.listen( (String link) { _processDeepLink(link, router); }, onError: (err) { debugPrint('Deep link error: $err'); }, ); } static Future _processDeepLink( String link, GoRouter router, ) async { final Uri uri = Uri.parse(link); try { // For Universal Links/App Links, use go_router's built-in handling if (uri.scheme == 'https') { // Construct the path from the URI final String path = uri.path; final String? queryString = uri.query.isNotEmpty ? '?${uri.query}' : ''; // Use go_router to navigate router.go('$path$queryString'); } else if (uri.scheme == 'myapp') { // Custom scheme handling router.go(uri.path, extra: uri.queryParameters); } } catch (e) { debugPrint('Error processing deep link: $e'); } } } ``` ### Initialize in Main App Initialize deep link handling in your main app: ```dart class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { late final GoRouter _router; @override void initState() { super.initState(); _router = _createRouter(); // Initialize deep link handling after router is created WidgetsBinding.instance.addPostFrameCallback((_) { DeepLinkManager.initializeDeepLinks(_router); }); } GoRouter _createRouter() { return GoRouter( routes: [ // Your routes ], ); } @override Widget build(BuildContext context) { return MaterialApp.router( title: 'My App', routerConfig: _router, ); } } ``` ## Full Working Example ### Complete App Implementation Here's a complete example app with deep linking: ```dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:uni_links/uni_links.dart'; import 'dart:async'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { late final GoRouter _router = _createRouter(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _initializeDeepLinks(); }); } GoRouter _createRouter() { return GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ GoRoute( path: 'product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: 'user/:userId', builder: (context, state) { final userId = state.pathParameters['userId']!; return UserProfileScreen(userId: userId); }, ), ], ), ], errorBuilder: (context, state) => const NotFoundScreen(), ); } Future _initializeDeepLinks() async { final String? initialLink = await getInitialLink(); if (initialLink != null) { _router.go(_parseDeepLink(initialLink)); } linkStream.listen((String link) { _router.go(_parseDeepLink(link)); }); } String _parseDeepLink(String link) { final uri = Uri.parse(link); return uri.path; } @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Deep Link App', routerConfig: _router, ); } } // Screens class HomeScreen extends StatelessWidget { const HomeScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Home')), body: const Center(child: Text('Home Screen')), ); } } class ProductScreen extends StatelessWidget { final String productId; const ProductScreen({Key? key, required this.productId}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Product')), body: Center( child: Text('Product ID: $productId'), ), ); } } class UserProfileScreen extends StatelessWidget { final String userId; const UserProfileScreen({Key? key, required this.userId}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('User Profile')), body: Center( child: Text('User ID: $userId'), ), ); } } class NotFoundScreen extends StatelessWidget { const NotFoundScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Not Found')), body: const Center( child: Text('Page not found'), ), ); } } ``` ## Best Practices ### Route Organization - Keep route definitions modular - Extract routes into separate files - Use descriptive path names - Group related routes together - Document route parameters - Version routes if needed ### Error Handling - Always provide error routes - Log invalid deep links - Provide fallback navigation - Validate parameters - Handle null safely - Test edge cases **Pro Tips** - Use RouteMatch to debug route parsing - Test deep links with adb on Android and xcode-select on iOS - Preserve state during navigation with keepAlive - Use query parameters for optional data - Combine with Riverpod or GetX for state management ## Master Complete Deep Linking You now have the knowledge to implement complete deep linking with go_router. Learn about deferred deep linking for advanced attribution and app install tracking. - [Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [Learn More](https://redirectly.app/deferred-deep-linking.md) ## Related Articles - [iOS Universal Links Setup](https://redirectly.app/blog/flutter-universal-links-ios-setup.md) — Complete guide to configuring Universal Links in Flutter with Xcode and AASA files. - [Android App Links Setup](https://redirectly.app/blog/flutter-android-app-links-setup.md) — Step-by-step guide to setting up App Links with assetlinks.json and SHA-256. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Universal Links in React Native: Complete iOS Setup Guide > Master Universal Links in React Native with Xcode configuration, AASA file setup, entitlements, AppDelegate code, and real device testing on iOS. - Canonical: https://redirectly.app/blog/react-native-universal-links-ios - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-03-05 - Author: Redirectly - Category: React Native · 8 min read Learn how to implement Universal Links in React Native with comprehensive Xcode configuration, AASA file setup, and best practices for iOS deep linking. Master associated domains, entitlements, and real device testing. ## What Are Universal Links? **Definition** Universal Links are HTTP or HTTPS URLs that seamlessly open your iOS app if it's installed, or open the website if it's not. They're Apple's standard way of handling deep links on iOS, providing a smooth user experience without the app chooser dialog. ### Universal Links Benefits - No app chooser dialog - Works with HTTPS URLs - Fallback to website - Seamless user experience - Shareable standard URLs - Great for marketing ### URL Schemes Comparison - **Custom Schemes** — `myapp://product/123` — Only if app installed - **Universal Links** — `https://myapp.com/product/123` — App or website ## How Universal Links Work in React Native ### The Universal Link Flow 1. **User Clicks Link** — User taps an HTTPS link from Safari, email, or another app 2. **iOS Checks AASA File** — iOS fetches apple-app-site-association from your domain 3. **Domain Verification** — iOS verifies domain is associated with your app via team ID and bundle ID 4. **App Opens Directly** — If verified, app opens and receives the URL. Otherwise, Safari opens the URL **Key Point:** iOS caches the AASA file, so changes may take time to propagate on devices. For new domains, it can take up to 2-3 hours to fully cache. ## Xcode Configuration & Entitlements ### Step 1: Add Associated Domains Entitlement In Xcode: 1. Open your project in Xcode 2. Select your app target 3. Go to "Signing & Capabilities" tab 4. Click "+ Capability" 5. Search for and add "Associated Domains" 6. Under "Domains", add your domain with "applinks:" prefix ```text applinks:myapp.com applinks:www.myapp.com applinks:api.myapp.com # For subdomains applinks:*.myapp.com ``` **Note:** Include all domain variants you might use. iOS is strict about domain matching. ### Step 2: Verify Entitlements.plist Xcode automatically generates an Entitlements.plist file. Verify it contains: ```xml com.apple.developer.associated-domains applinks:myapp.com applinks:www.myapp.com ``` ### Step 3: Update Build Settings Required Settings: - **Code Signing Identity:** Apple Development certificate - **Team ID:** Must match your Apple Developer account - **Bundle Identifier:** Must be registered in Apple Developer - **Provisioning Profile:** Includes "Associated Domains" capability ## Creating the AASA File The apple-app-site-association (AASA) file is a JSON file hosted on your domain that tells iOS which paths your app should handle. This is the critical piece that enables Universal Links. ### AASA File Structure ```json { "applinks": { "apps": [], "details": [ { "appID": "ABC123DEF456.com.myapp.ios", "paths": [ "/product/*", "/user/:userId", "/search/*", "NOT /admin/*" ] }, { "appID": "ABC123DEF456.com.myapp.beta", "paths": [ "/beta/*" ] } ] } } ``` **Find Your App ID:** Your appID is Team ID (from Apple Developer) + Bundle Identifier Example: ABC123DEF456.com.myapp.ios ### File Hosting Requirements **Location: /.well-known/** ```text https://myapp.com/.well-known/apple-app-site-association https://www.myapp.com/.well-known/apple-app-site-association ``` The file must be served from all domain variants you add to Associated Domains. **HTTP Headers Required:** ```bash # Nginx example location /.well-known/apple-app-site-association { add_header Content-Type application/json; add_header Access-Control-Allow-Origin *; } # Apache example Header set Content-Type "application/json" Header set Access-Control-Allow-Origin "*" ``` ### Validate Your AASA File Use Our AASA Validator Tool — [Check your AASA configuration](https://redirectly.app/aasa-validator.md) ## React Native Code Implementation Set up React Navigation to handle incoming universal links from iOS. ```typescript import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { Linking } from 'react-native'; import { useEffect } from 'react'; const linking = { prefixes: ['https://myapp.com', 'https://www.myapp.com'], config: { screens: { Home: '', Product: 'product/:id', UserProfile: 'user/:userId', NotFound: '*', }, }, async getInitialURL() { // Handle notification-opened while app was killed const url = await Linking.getInitialURL(); if (url != null) { return url; } }, }; export function RootNavigator() { const navigationRef = useRef(null); useEffect(() => { const listening = Linking.addEventListener('url', ({ url }) => { navigationRef.current?.resetRoot({ index: 0, routes: [ { name: 'Home', state: { routes: [ { name: 'Product', params: { id: extractProductId(url) }, }, ], }, }, ], }); }); return () => listening.remove(); }, []); return ( {/* Your screens */} ); } function extractProductId(url: string): string { return url.split('/product/')[1]; } ``` ## AppDelegate Configuration In some cases, you may need to manually handle deep links in AppDelegate, especially if using custom RCTLinkingManager handling. ```swift import UIKit import React @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let rootViewController = RCTRootViewController() rootViewController.bundle = Bundle(url: jsCodeLocation) self.window = UIWindow(frame: UIScreen.main.bounds) self.window?.rootViewController = rootViewController self.window?.makeKeyAndVisible() return true } // Handle universal links func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { // React Navigation will handle the routing RCTLinkingManager.application(application, open: url, sourceApplication: nil, annotation: [:]) return true } return false } } ``` ## Testing on Real Devices ### Method 1: Testing via Notes App 1. Build and deploy your app to a real device 2. Open Notes app on the device 3. Create a new note and paste your universal link 4. Long-press the link (don't tap yet) 5. Select "Open" or tap the link 6. App should open directly to the intended screen `https://myapp.com/product/123` ### Method 2: Testing via Safari 1. Open Safari on your test device 2. Navigate to your universal link URL 3. iOS should intercept and open your app 4. If it opens the website instead, check AASA file 5. Try force-closing the app and retesting **Wait 2-3 hours:** iOS caches AASA files. New domains may not work immediately. ### Method 3: Using Apple's Diagnostic Tool ```bash # On your Mac, check the device logs log stream --predicate 'eventMessage contains[c] "universal link" or eventMessage contains[c] "applinks"' --level debug # Or check via Xcode Console while running # Build & Run your app, then view Console output for linking events ``` ## Common Issues & Fixes ### Issue: Link Opens in Safari Instead of App **Possible Causes:** - AASA file not properly hosted - Domain not in Associated Domains list - Incorrect App ID in AASA file - AASA file not cached yet **Solutions:** - Verify AASA file with our validator tool - Check Content-Type header is application/json - Force remove app from device and reinstall - Wait 2-3 hours for iOS cache to clear - Add domain to Settings → Developer on device ### Issue: App Crashes When Opening Deep Link **Possible Causes:** - URL parameters not properly extracted - Navigation route doesn't exist - Missing error handling in navigation **Solutions:** - Add error boundaries in navigation - Log incoming URLs: console.log(url) - Validate parameters before navigation - Use try-catch in getInitialURL handler ### Issue: Associated Domains Capability Missing **Solutions:** - In Xcode, go to Signing & Capabilities - Click + Capability and search "Associated" - Add your domain with applinks: prefix - Ensure provisioning profile includes capability - Clean build folder and rebuild ### Need Help? Use our AASA Validator to check your configuration: [Validate Your AASA File](https://redirectly.app/aasa-validator.md) ## Deferred Deep Links Integration **Combine with Deferred Deep Links** Universal Links are great for installed apps, but for users who don't have your app yet, deferred deep links ensure they get the intended experience after installation. [Learn About Deferred Deep Links](https://redirectly.app/react-native-deferred-deep-linking.md) ## Universal Links are Worth It! Setting up Universal Links takes effort, but it significantly improves the user experience by providing seamless navigation from web to app. Combined with proper deferred deep linking, you have a complete solution for all user scenarios. ## Related - [React Native Deep Linking with React Navigation v6](https://redirectly.app/blog/react-native-deep-linking-react-navigation.md) — Master deep linking in React Native with linking configuration, TypeScript examples, and testing strategies. - [What is Deferred Deep Linking?](https://redirectly.app/blog/what-is-deferred-deep-linking.md) — Learn how deferred deep linking works and how to implement it in your React Native app. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Migrate from Firebase Dynamic Links to Redirectly in React Native > A step-by-step React Native guide to migrate from Firebase Dynamic Links to Redirectly, with TypeScript code examples, config, and troubleshooting. - Canonical: https://redirectly.app/blog/react-native-firebase-dynamic-links-migration - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-12-18 - Author: Dmytro Vyrych - Category: React Native · 8 min read Complete step-by-step guide to migrate your React Native app from Firebase Dynamic Links to Redirectly. Includes TypeScript code examples and troubleshooting tips. **Migration Timeline: 15-30 Minutes** — This guide covers a basic migration. Complex setups with custom logic may require additional time for testing and verification. ## Prerequisites You need: - React Native 0.60 or higher - Node.js and npm/yarn - Existing Firebase Dynamic Links setup - Redirectly account (sign up free at redirectly.app) Time estimate: 15-30 minutes for basic migration, plus testing time ## Step 1: Remove Firebase Dynamic Links ### Uninstall Firebase Package Remove the firebase/dynamic-links package from your project: ```bash npm uninstall @react-native-firebase/dynamic-links firebase # or if using yarn yarn remove @react-native-firebase/dynamic-links firebase ``` ### Remove Firebase Configuration Find and remove any Firebase initialization code from your app. Look for files like: - src/services/firebaseService.ts - src/utils/deepLinking.ts - Any imports of dynamicLinks from Firebase You can safely delete files that were only used for Firebase Dynamic Links initialization. ## Step 2: Install Redirectly SDK ### Install from npm Add Redirectly SDK to your project: ```bash npm install @redirectly/react-native # or if using yarn yarn add @redirectly/react-native ``` ### Native Linking (if needed) For React Native 0.60+, autolinking handles most of the setup. If you need manual linking: ```bash cd ios && pod install && cd .. # or for android cd android && ./gradlew clean ``` ## Step 3: Initialize Redirectly ### Create Redirectly Service Create a new file to manage Redirectly initialization: ```typescript import { Redirectly, DeepLinkData } from '@redirectly/react-native'; export class DeepLinkingService { private static initialized = false; static async initialize(apiKey: string): Promise { if (this.initialized) return; await Redirectly.init({ apiKey: apiKey, // Optional: set user for attribution tracking userId: null, }); this.initialized = true; } static async createLink( path: string, campaign?: string, data?: Record ): Promise { const link = await Redirectly.createLink({ path, campaign, data: data || {}, }); return link.url; } static onDeepLink( callback: (deepLink: DeepLinkData) => void ): () => void { return Redirectly.onDeepLink(callback); } static async getInitialLink(): Promise { return Redirectly.getInitialLink(); } } ``` ### Initialize in App.tsx Call the initialization in your main app component: ```typescript import React, { useEffect } from 'react'; import { SafeAreaView } from 'react-native'; import { DeepLinkingService } from './services/DeepLinkingService'; export default function App() { useEffect(() => { // Initialize Redirectly with your API key DeepLinkingService.initialize('your-api-key-here'); }, []); return ( {/* Your app content */} ); } ``` ## Step 4: Migrate Deep Link Listener ### Old Firebase Code Your existing Firebase implementation: ```typescript import { useEffect } from 'react'; import dynamicLinks from '@react-native-firebase/dynamic-links'; import { useNavigation } from '@react-navigation/native'; export const useFirebaseDeepLinks = () => { const navigation = useNavigation(); useEffect(() => { let unsubscribe: (() => void) | undefined; dynamicLinks() .getInitialLink() .then((link) => { if (link?.url != null) { handleDeepLink(link.url); } }); unsubscribe = dynamicLinks().onLink((link) => { const { url } = link; handleDeepLink(url); }); return () => unsubscribe?.(); }, [navigation]); const handleDeepLink = (url: string) => { const parsedUrl = new URL(url); const path = parsedUrl.pathname; if (path.startsWith('/product/')) { const productId = path.split('/')[2]; navigation.navigate('ProductScreen', { productId }); } }; }; ``` ### New Redirectly Code Migrated to use Redirectly: ```typescript import { useEffect } from 'react'; import { useNavigation } from '@react-navigation/native'; import { DeepLinkingService } from '../services/DeepLinkingService'; import { DeepLinkData } from '@redirectly/react-native'; export const useRedirectlyDeepLinks = () => { const navigation = useNavigation(); useEffect(() => { // Handle initial deep link when app is launched from terminated state DeepLinkingService.getInitialLink().then((deepLink) => { if (deepLink) { handleDeepLink(deepLink); } }); // Listen for deep links when app is already running const unsubscribe = DeepLinkingService.onDeepLink((deepLink) => { handleDeepLink(deepLink); }); return unsubscribe; }, [navigation]); const handleDeepLink = (deepLink: DeepLinkData) => { const { path, campaign, data } = deepLink; // Log attribution data if (campaign) { console.log('User came from campaign:', campaign); console.log('Additional data:', data); } // Route based on path if (path.startsWith('/product/')) { const productId = path.split('/')[2]; navigation.navigate('ProductScreen', { productId }); } else if (path.startsWith('/user/')) { const userId = path.split('/')[2]; navigation.navigate('UserProfile', { userId }); } else { navigation.navigate('Home'); } }; }; ``` ## Step 5: Migrate Link Creation ### Old Firebase Pattern Creating links with Firebase: ```typescript const shareProduct = async (productId: string, productName: string) => { const link = await dynamicLinks().buildLink({ link: `https://yourdomain.com/product/${productId}`, domainUriPrefix: 'https://yourdomain.page.link', android: { packageName: 'com.example.myapp', fallbackUrl: 'https://yourdomain.com/fallback', }, ios: { bundleId: 'com.example.MyApp', fallbackUrl: 'https://yourdomain.com/fallback', appStoreId: '123456789', }, }); return link; }; ``` ### New Redirectly Pattern Creating links with Redirectly: ```typescript const shareProduct = async (productId: string, productName: string) => { const linkUrl = await DeepLinkingService.createLink( `/product/${productId}`, 'social_share', // campaign identifier { productName, timestamp: new Date().toISOString(), } ); return linkUrl; }; ``` ## Step 6: Testing & Deployment ### Test Deep Linking **1. Build and run your app on a test device:** ```bash npm run android # or for iOS npm run ios ``` **2. Test initial deep link:** ```bash adb shell am start -W -a android.intent.action.VIEW -d "redirectly://product/123" com.example.myapp ``` **3. Test deep link while app is running:** Create a link via Redirectly dashboard, share it, and tap to verify navigation works. ### Checklist Before Deploying - All Firebase Dynamic Links imports removed - Redirectly SDK installed and initialized - Deep link listener migrated and tested - Link creation replaced with Redirectly.createLink - Tested on physical device (not just simulator) - Verified attribution tracking works ## Migration Complete! Your React Native app is now using Redirectly instead of Firebase Dynamic Links. Check out our advanced guides for more features. - [Advanced Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) - [Feature Comparison](https://redirectly.app/blog/firebase-dynamic-links-vs-redirectly.md) ## Related - [Firebase Dynamic Links Shutdown Timeline](https://redirectly.app/blog/firebase-dynamic-links-shutdown.md) — Complete timeline and impact analysis of the Firebase Dynamic Links deprecation. - [Firebase Dynamic Links vs Redirectly](https://redirectly.app/blog/firebase-dynamic-links-vs-redirectly.md) — Feature-by-feature comparison of both platforms with code examples. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # assetlinks.json: Complete Guide for Android App Links > Master assetlinks.json for Android App Links. Learn Digital Asset Links format, SHA-256 certificate fingerprints, and hosting requirements. - Canonical: https://redirectly.app/blog/assetlinks-json-guide - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-20 - Author: Redirectly - Category: Android · 8 min read Master Digital Asset Links and enable seamless deep linking for your Android apps ## What are Digital Asset Links? Digital Asset Links is Google's solution for verifying the relationship between your app and your website. The assetlinks.json file proves that your Android app and your website are owned by the same organization, enabling App Links—Android's equivalent to iOS Universal Links. When a user clicks a link to your domain on an Android device, the system checks for your assetlinks.json file. If the file exists and your app is installed, Android directs users to the app. If your app isn't installed, the link opens in the browser. Unlike iOS where trust is implicit, Android requires cryptographic verification via your app's signing certificate. This is why generating the correct SHA-256 fingerprint is critical for Digital Asset Links. ## assetlinks.json Format Reference Here's the complete structure of an assetlinks.json file: ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ] } } ] ``` ### Field Explanations - **relation:** The type of relationship. Use "delegate_permission/common.handle_all_urls" for App Links - **namespace:** Always "android_app" for Android apps - **package_name:** Your app's exact package name (e.g., com.example.myapp) - **sha256_cert_fingerprints:** Array of SHA-256 fingerprints for your signing certificates ## Relation Types and Target Objects Digital Asset Links supports different relations for various purposes: ### delegate_permission/common.handle_all_urls The primary relation for App Links. Tells Android your app should handle all links to your domain (when matching intent filters are present). ### delegate_permission/common.get_login_creds Enables credential sharing with the app. Allows Android to autofill passwords in your app when they're stored in Chrome. ### Custom Relations You can define custom relations for app-specific purposes. Format: "custom_relation_name" ## SHA-256 Certificate Fingerprints The SHA-256 fingerprint is the most critical part of assetlinks.json. You need to generate three different fingerprints: ### 1. Debug Signing Certificate Used when testing your app during development. Android automatically creates this when you first run your app on an emulator or device. ```bash keytool -list -v -keystore ~/.android/debug.keystore \ -alias androiddebugkey -storepass android -keypass android ``` ### 2. Release Signing Certificate Your keystore file used to sign the release APK/AAB. Keep this secure! ```bash keytool -list -v -keystore /path/to/your/keystore.jks -alias YOUR_ALIAS ``` ### 3. Play App Signing Certificate When you upload to Google Play Store, Google re-signs your app with their certificate. You must use this fingerprint for App Links to work on production apps! **Important:** Find your Play App Signing certificate in Google Play Console under Settings >> App Signing. Copy the SHA-256 certificate fingerprint. ### Understanding the Output ```text Alias name: androiddebugkey Creation date: Dec 1, 2023 Entry type: PrivateKeyEntry Certificate fingerprints: MD5: 12:34:56:78:9A:BC:DE:F0:12:34:56:78:9A:BC:DE:F0 SHA1: 12:34:56:78:9A:BC:DE:F0:12:34:56:78:9A:BC:DE:F0:12:34:56:78 SHA256: AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99 SHA512: ... ``` Copy only the SHA256 line (with the colons). You'll need just the hex string without "SHA256:" prefix for assetlinks.json. ## Multiple Apps in One File If you have multiple apps (production, beta, staging), include them all in one assetlinks.json: ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp", "sha256_cert_fingerprints": ["PROD_FINGERPRINT"] } }, { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp.beta", "sha256_cert_fingerprints": ["BETA_FINGERPRINT"] } } ] ``` ## Hosting Requirements Your assetlinks.json file must be hosted correctly for Android to validate it: ### File Location ```text https://yourdomain.com/.well-known/assetlinks.json ``` The file must be in .well-known directory at the root of your domain. No alternate paths work. ### HTTPS Required Your domain must use HTTPS. Android will not read assetlinks.json over HTTP. ### Content-Type Header ```text Content-Type: application/json ``` Verify this header is returned by your web server. ### Valid JSON Must be valid JSON. Syntax errors cause silent failures. ### No Redirects Android won't follow redirects. The file must be directly accessible at the exact URL. ## Complete Production Example Here's a production-ready assetlinks.json with debug and release certificates: ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99", "11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00" ] } } ] ``` ## Validating Your assetlinks.json File After creating your file, validate it before deployment: ### 1. JSON Validation Use a JSON validator to check syntax. ### 2. Use Redirectly's Validator Our tool validates assetlinks.json and catches common mistakes. ### 3. Verify Hosting Check that your server returns application/json content-type over HTTPS. ### 4. Test on Device Install your app and test tapping links to verify they open in your app. **Pro Tip:** Use Redirectly's free assetlinks.json validator to catch errors quickly. [Try assetlinks.json Validator](https://redirectly.app/assetlinks-validator.md) ## Troubleshooting Common Issues Here are solutions to the most common assetlinks.json problems: ### Wrong SHA-256 fingerprint This is the #1 issue. Verify you're using the Play App Signing certificate, not your upload certificate. Check Google Play Console for the correct fingerprint. ### File at wrong location assetlinks.json must be at .well-known/assetlinks.json. Not in a different directory, not in the root. ### Content-Type header incorrect Test with: curl -I https://yourdomain.com/.well-known/assetlinks.json | grep Content-Type ### Invalid JSON Copy your file to a JSON validator. Even a missing comma breaks everything. ### Links not opening in app after fixes Uninstall and reinstall your app. Android caches assetlinks.json verification results aggressively. ## App Manifest Configuration assetlinks.json is only half of the setup. Your AndroidManifest.xml must also declare intent filters: ```xml ``` **Important:** Include android:autoVerify="true" so Android verifies your assetlinks.json when the app is installed. ## Building on assetlinks.json: Next Steps Now that you understand assetlinks.json, explore related deep linking topics: - [Deferred Deep Linking in Flutter](https://redirectly.app/flutter-deferred-deep-linking.md) — Combine assetlinks.json with deferred deep linking for comprehensive Android experiences. - [assetlinks.json Validator Tool](https://redirectly.app/assetlinks-validator.md) — Validate your assetlinks.json configuration instantly with our free validator. ## Summary assetlinks.json is essential for Android App Links. By understanding Digital Asset Links, certificate fingerprints, and hosting requirements, you can create reliable deep links that seamlessly direct users to your app. Remember: - Host assetlinks.json at .well-known/assetlinks.json with HTTPS - Use your Play App Signing certificate SHA-256 fingerprint - Include all app variants (production, beta, staging) - Ensure valid JSON and application/json content-type - Add android:autoVerify="true" to your intent filters - Uninstall and reinstall to force verification after changes --- Full site index for AI agents: https://redirectly.app/llms.txt --- # AASA File Format for iOS: Complete Developer Guide > Master the Apple App Site Association (AASA) file format. Learn JSON structure, path patterns, and hosting requirements for iOS universal links. - Canonical: https://redirectly.app/blog/aasa-file-format-ios-guide - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-15 - Author: Redirectly - Category: iOS · 8 min read Master the Apple App Site Association file format and unlock universal links for your iOS apps ## What is AASA and Why It Exists The Apple App Site Association (AASA) file is a JSON configuration file that enables iOS apps to claim ownership of your website domains. Without this file, iOS apps can open your website links, but users won't get the seamless experience of being directed directly to app content—they'll see a web page instead. AASA is the foundation of Universal Links, Apple's solution for deep linking. It works by establishing a cryptographic connection between your website and your app, proving that both entities are owned by the same organization. This trust relationship is essential for Apple's strict security model. When users tap a link to your domain, iOS checks for your AASA file. If it finds the correct configuration and your app is installed, iOS opens the app directly to the specified content. If the app isn't installed, the link opens normally in Safari. ## AASA JSON Format Reference Here's a complete AASA file structure with annotations explaining each section: ```json { "applinks": { "apps": [], "details": [ { "appID": "9ABC123DEF.com.example.myapp", "paths": [ "/product/*", "/account/profile", "NOT /static/*" ] } ] }, "webcredentials": { "apps": ["9ABC123DEF.com.example.myapp"] } } ``` ### Field Explanations - **appID:** Your Team ID + Bundle Identifier (format: TeamID.bundleIdentifier) - **paths:** URL paths your app handles. Use * for wildcards, NOT for exclusions - **webcredentials:** Enables password autofill for password managers (optional) ## applinks vs webcredentials Sections Your AASA file can contain two main sections, each serving different purposes: ### applinks Enables Universal Links. When users tap links to your domain, iOS directs them to your app if it's installed. This is your primary deep linking mechanism. ### webcredentials Enables AutoFill for passwords and passkeys in Safari and your app. This improves user authentication experience but doesn't directly affect deep linking. ## Version Differences: iOS 13+ vs Legacy Apple introduced significant changes to AASA in iOS 13. Here's how they differ: | Feature | iOS 13+ | iOS 12 and Earlier | | --- | --- | --- | | Format | JSON with "details" array | Simple JSON format | | Multiple apps | Supported (details array) | Limited support | | Negative paths | Fully supported with NOT | No support | | Case sensitivity | Case-sensitive matching | Case-insensitive | For modern app development, focus on the iOS 13+ format. If you need to support older iOS versions, structure your AASA to be backward compatible while leveraging newer features. ## Path Patterns and Wildcards Path patterns are how you tell iOS which URLs your app should handle. Understanding the pattern matching rules is critical: ```json "paths": [ "/product/*", // Wildcard - matches any substring "/account/profile", // Exact path match "/posts/*/comments", // Multi-level wildcards "NOT /admin/*", // Exclude paths from matching "NOT /login", // Exclude specific routes "/?*", // Match root and all subpaths "/checkout/success/*" // Deep path matching ] ``` ### Wildcard Rules - `*` matches zero or more characters in a single path component - `?` matches exactly one character - `NOT` prefix excludes paths from being handled by your app - Paths are case-sensitive in iOS 13+ - Query strings and fragments are ignored in path matching ## Hosting Requirements Your AASA file must be hosted correctly for iOS to find and validate it: ### File Location ```text https://yourdomain.com/.well-known/apple-app-site-association ``` The file must be in the .well-known directory at the root of your domain. ### HTTPS Required Your website must use HTTPS. iOS will not read AASA files over HTTP. ### Content-Type Header ```text Content-Type: application/json ``` Your server must return the correct MIME type. ### Valid JSON The file must be valid JSON. Invalid JSON will silently fail, and iOS will fall back to opening links in Safari. ### No File Extension The file should have no .json extension. Name it exactly: apple-app-site-association ## Complete AASA Example Here's a production-ready AASA configuration: ```json { "applinks": { "apps": [], "details": [ { "appID": "ABC123DEF9.com.example.app", "paths": [ "/product/*", "/order/*", "/account/*", "/referral/*", "NOT /admin/*", "NOT /api/*" ] }, { "appID": "ABC123DEF9.com.example.app.beta", "paths": ["/beta/*"] } ] }, "webcredentials": { "apps": [ "ABC123DEF9.com.example.app", "ABC123DEF9.com.example.app.beta" ] } } ``` ## Validating Your AASA File After creating your AASA file, validate it before deploying. Invalid AASA files won't throw errors—they'll silently fail. Here are the validation steps: ### 1. Check JSON Validity Use a JSON validator to ensure your file has valid JSON syntax. ### 2. Use Redirectly's AASA Validator Test your file with our free validator tool, which checks iOS 13+ compatibility and provides detailed error messages. ### 3. Verify HTTP Headers Ensure your server returns Content-Type: application/json and proper HTTPS. ### 4. Test on Device Install your app and test tapping links to ensure they open in your app correctly. **Pro Tip:** Use Redirectly's free AASA validator to quickly test your configuration before deploying to production. [Try AASA Validator](https://redirectly.app/aasa-validator.md) ## Troubleshooting Common Issues Here are solutions to the most common AASA problems: ### Links not opening in app Check: (1) AASA is at .well-known/apple-app-site-association, (2) appID matches your Team ID + Bundle ID, (3) paths match your URLs, (4) HTTPS is enabled ### Content-Type header incorrect Verify your web server returns application/json. Test with: curl -I https://yourdomain.com/.well-known/apple-app-site-association ### Invalid JSON syntax Copy your AASA content into a JSON validator. Even a missing comma or quote will cause silent failures. ### Still not working after fixes Delete and reinstall the app. iOS caches AASA files aggressively. Clearing the app cache forces iOS to re-fetch the configuration. ## Building on AASA: Next Steps Now that you understand AASA, explore related deep linking topics: - [Deferred Deep Linking in Flutter](https://redirectly.app/flutter-deferred-deep-linking.md) — Combine AASA with deferred deep linking to create seamless user experiences when your app isn't yet installed. - [AASA Validator Tool](https://redirectly.app/aasa-validator.md) — Validate your AASA configuration instantly with our free online validator. ## Summary The AASA file is essential for iOS deep linking. By understanding its format, hosting requirements, and path patterns, you can create reliable universal links that seamlessly direct users to your app content. Remember: - Host AASA at .well-known/apple-app-site-association with HTTPS - Ensure valid JSON and application/json Content-Type - Use appID format: TeamID.bundleIdentifier - Define paths carefully with wildcards and NOT exclusions - Validate before deploying using our validator tool --- Full site index for AI agents: https://redirectly.app/llms.txt --- # React Native Deep Linking with React Navigation v6: Complete Tutorial > Master deep linking in React Native with React Navigation v6. Learn linking config, screen mapping, TypeScript examples, and testing strategies. - Canonical: https://redirectly.app/blog/react-native-deep-linking-react-navigation - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-03-01 - Author: Redirectly - Category: React Native · 8 min read Learn how to implement deep linking in React Native apps using React Navigation v6. Master linking configuration, screen mapping with TypeScript, handling deferred deep links, and testing strategies for both iOS and Android platforms. ## Understanding Deep Linking in React Native **What is Deep Linking in React Native?** Deep linking in React Native allows your app to handle URLs and route them to specific screens. When users click a link from email, SMS, push notifications, or web pages, React Navigation intercepts that URL and navigates to the appropriate screen with the relevant data. ### URL Schemes (Custom Protocol) Custom URL schemes like 'myapp://' let you handle app-specific links. `myapp://product/123` — Works when app is installed ### Universal Links & App Links Standard HTTPS URLs with fallback to web when app isn't installed. `https://myapp.com/product/123` — Seamless iOS & Android experience React Navigation v6 provides a unified API to handle both URL schemes and universal/app links, making it easy to implement deep linking across iOS and Android with a single codebase. ## React Navigation Linking Configuration React Navigation v6 uses a 'linking' prop on the NavigationContainer to define how URLs map to navigation state. ### Basic Linking Configuration ```typescript import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; const Stack = createNativeStackNavigator(); const linking = { prefixes: ['myapp://', 'https://myapp.com', 'https://*.myapp.com'], config: { screens: { Home: '/', Product: 'product/:id', User: 'user/:userId', NotFound: '*', }, }, }; export default function RootNavigator() { return ( ); } ``` **Prefixes:** Define the URL schemes and domains your app responds to. Use wildcard subdomains for branch links or deep link services. ## Screen Mapping from URL Paths React Navigation maps URL patterns to screens and extracts parameters. The 'config.screens' object defines these mappings using route patterns. ### Advanced Configuration Example ```typescript interface RootStackParamList { Home: undefined; Product: { id: string }; Cart: { items?: string[] }; Profile: { userId: string }; Settings: { section?: string }; } const linking = { prefixes: ['myapp://', 'https://myapp.com'], config: { screens: { // Exact matches Home: '', // Dynamic parameters Product: 'product/:id', // Multiple parameters Cart: 'cart/:itemId', // Optional parameters Profile: 'user/:userId', Settings: { path: 'settings/:section?', parse: { section: (section: string) => section?.toLowerCase(), }, }, // Nested navigators Account: { screens: { Orders: 'account/orders', Wishlist: 'account/wishlist/:categoryId?', }, }, // Fallback for unmapped routes NotFound: '*', }, }, }; ``` ### URL Examples - `myapp://product/123` → Product screen with id='123' - `https://myapp.com/user/john` → Profile screen with userId='john' - `myapp://settings/account` → Settings with section='account' ### Accessing Parameters ```typescript type ProductScreenProps = NativeStackScreenProps< RootStackParamList, 'Product' >; function ProductScreen({ route, navigation }: ProductScreenProps) { const { id } = route.params; useEffect(() => { // Fetch product with id fetchProduct(id); }, [id]); return ( Product {id} ); } ``` ## Handling Deferred Deep Links with Redirectly Deferred deep links handle cases where users click a link before installing your app. When they install and open the app for the first time, it should navigate to the intended screen. Redirectly makes this seamless. ### Integrating Redirectly for Deferred Deep Links ```typescript import { NavigationContainer } from '@react-navigation/native'; import { useEffect } from 'react'; import Redirectly from '@redirectly/react-native'; const linking = { prefixes: ['myapp://', 'https://myapp.com'], config: { screens: { Home: '', Product: 'product/:id', User: 'user/:userId', }, }, // Custom async handler for deferred deep links async getInitialURL() { // 1. Check if app was opened from a deep link (cold start) const url = await Linking.getInitialURL(); if (url != null) { return url; } // 2. Check Redirectly for deferred deep link try { const redirectlyUrl = await Redirectly.getInitialURL(); return redirectlyUrl; } catch (error) { console.error('Error fetching from Redirectly:', error); } // 3. Default to home screen return undefined; }, }; function RootNavigator() { useEffect(() => { // Handle deep links when app is already running const unsubscribe = Linking.addEventListener('url', ({ url }) => { // Use React Navigation to navigate const route = url.replace(/.*?:/\//, ''); // Navigation logic here }); return unsubscribe.remove; }, []); return ( {/* Navigation stacks */} ); } ``` **Benefit:** Redirectly handles the deferred deep link attribution, so your app knows the original intent even if the user installed from an app store link. ### Learn More About Deferred Deep Linking [Read the Complete Deferred Deep Linking Guide](https://redirectly.app/react-native-deferred-deep-linking.md) ## TypeScript Implementation Using TypeScript ensures type safety throughout your deep linking implementation. Here's a complete, production-ready example. ```typescript import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; // Type-safe parameter definitions export type RootStackParamList = { Home: undefined; Product: { id: string; category?: string }; UserProfile: { userId: string; tab?: 'orders' | 'reviews' }; Search: { query: string }; NotFound: undefined; }; // Screen-specific props type export type ProductScreenProps = NativeStackScreenProps< RootStackParamList, 'Product' >; // Complete linking configuration with full type safety export const linking = { prefixes: ['myapp://', 'https://myapp.com', 'https://www.myapp.com'], config: { screens: { Home: { path: '', }, Product: { path: 'product/:id', parse: { id: (id: string) => id, category: (category?: string) => category, }, }, UserProfile: { path: 'user/:userId', parse: { userId: (userId: string) => userId, tab: (tab?: string) => tab as 'orders' | 'reviews' | undefined, }, }, Search: { path: 'search/:query', parse: { query: (query: string) => decodeURIComponent(query), }, }, NotFound: '*', }, }, }; // Type-safe hook for navigation export function useTypedNavigation() { return useNavigation>(); } // Example screen component function ProductScreen({ route }: ProductScreenProps) { const { id, category } = route.params; return ( Product: {id} {category && Category: {category}} ); } // Example programmatic navigation with type checking function navigateToProduct(navigation: any, id: string, category?: string) { navigation.navigate('Product', { id, category, }); } ``` ## Testing Deep Links on iOS and Android ### iOS Testing **Method 1: Using Simulator** ```bash # First, build and run your app in iOS simulator npx react-native run-ios # Then open a deep link in the simulator xcrun simctl openurl booted myapp://product/123 # Or with universal links xcrun simctl openurl booted https://myapp.com/product/123 ``` **Method 2: Testing Universal Links** Universal links require a valid apple-app-site-association file on your server. For testing: ```bash # Check if your AASA file is properly configured # Open in Safari on iOS device: https://yourdomain.com/apple-app-site-association # Validate with our AASA validator # Visit: https://redirectly.app/aasa-validator ``` ### Android Testing **Method 1: Using Android Emulator** ```bash # Build and run app on Android emulator npx react-native run-android # Open a deep link using adb adb shell am start -W -a android.intent.action.VIEW -d myapp://product/123 # Or with universal links adb shell am start -W -a android.intent.action.VIEW -d https://myapp.com/product/123 ``` **Method 2: Testing App Links** App Links require a valid assetlinks.json file on your server. ```bash # Validate your assetlinks.json with: # Visit: https://redirectly.app/assetlinks-validator # Check digital asset links on device adb shell am start -a android.intent.action.VIEW -d https://yourdomain.com/product/123 ``` ### Testing Checklist - Test cold start (app not running) - Test warm start (app already running) - Test with invalid/non-existent IDs - Test with special characters in parameters - Test both URL schemes and universal/app links - Test on real devices and emulators - Verify AASA/assetlinks files are accessible - Test deferred deep links with fresh install ## Related Resources & Tools - [AASA Validator](https://redirectly.app/aasa-validator.md) — Validate your apple-app-site-association configuration - [Assetlinks Validator](https://redirectly.app/assetlinks-validator.md) — Validate your assetlinks.json for Android App Links - [Deferred Deep Links](https://redirectly.app/react-native-deferred-deep-linking.md) — Complete guide to handling deferred deep links ## Master Deep Linking Today! Deep linking with React Navigation v6 is a powerful way to improve user engagement and create seamless app experiences. By following this guide and testing thoroughly on both platforms, you'll have a robust deep linking implementation that handles both standard and deferred deep links. ## Related - [Universal Links in React Native: Complete iOS Setup](https://redirectly.app/blog/react-native-universal-links-ios.md) — Deep dive into setting up Universal Links on iOS with AASA configuration and best practices. - [What is Deferred Deep Linking?](https://redirectly.app/blog/what-is-deferred-deep-linking.md) — Learn how deferred deep linking works and why it's essential for post-install user experiences. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Firebase Dynamic Links vs Redirectly > A complete feature-by-feature comparison of Firebase Dynamic Links and Redirectly, covering pricing, code examples, and which is the better fit. - Canonical: https://redirectly.app/blog/firebase-dynamic-links-vs-redirectly - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-12-20 - Author: Dmytro Vyrych - Category: Comparison · 8 min read Complete feature-by-feature comparison. See how Redirectly compares to Firebase Dynamic Links with code examples, pricing, and real-world use cases. ## Feature Comparison | Feature | Firebase Dynamic Links | Redirectly | | --- | --- | --- | | Deferred Deep Linking | Yes | Yes | | Install Attribution | Yes | Yes | | SDK Size | ~10-15 MB | ~2-3 MB | | Setup Time | 2-4 hours | 15-30 min | | Active Maintenance | No | Yes | | React Native Support | Yes | Yes | | Flutter Support | Yes | Yes | | Native iOS Support | Yes | Yes | | Native Android Support | Yes | Yes | | Transparent Pricing | Contact Sales | Free + Pay-as-you-go | | API Documentation | Archived | Active & Detailed | | Technical Support | No | Yes | ## Code Comparison ### React Native: Creating a Deep Link Firebase Dynamic Links: ```javascript import dynamicLinks from '@react-native-firebase/dynamic-links'; const buildDeepLink = async () => { const link = await dynamicLinks().buildLink({ link: 'https://yourdomain.com/product/123', domainUriPrefix: 'https://yourdomain.page.link', android: { packageName: 'com.example.myapp', fallbackUrl: 'https://yourdomain.com/fallback', }, ios: { bundleId: 'com.example.MyApp', fallbackUrl: 'https://yourdomain.com/fallback', appStoreId: '123456789', }, }); return link; }; ``` Redirectly: ```javascript import { Redirectly } from '@redirectly/react-native'; const buildDeepLink = async () => { const link = await Redirectly.createLink({ path: '/product/123', campaign: 'summer_sale', }); return link.url; }; ``` ### Listening for Deep Links on App Open Firebase Dynamic Links: ```javascript import dynamicLinks from '@react-native-firebase/dynamic-links'; import { useEffect } from 'react'; useEffect(() => { const unsubscribe = dynamicLinks().onLink((link) => { const { url } = link; handleDeepLink(url); }); dynamicLinks() .getInitialLink() .then((link) => { if (link?.url != null) { handleDeepLink(link.url); } }); return unsubscribe; }, []); ``` Redirectly: ```javascript import { Redirectly } from '@redirectly/react-native'; import { useEffect } from 'react'; useEffect(() => { const unsubscribe = Redirectly.onDeepLink((deepLinkData) => { const { path, campaign, data } = deepLinkData; handleDeepLink({ path, campaign, ...data }); }); return unsubscribe; }, []); ``` ## Pricing Comparison ### Firebase Dynamic Links - **Model:** Free with Firebase - **Cost:** Included with Firebase, but service is deprecated - **Limits:** No explicit limits, but no ongoing support Service ending August 5, 2025. No longer accepting new links. ### Redirectly - **Model:** Free + Pay-as-you-go - **Starter:** Free forever with up to 10,000 links/month - **Scale:** $0.001 per link after free tier Transparent pricing, no hidden fees, cancel anytime. ### Real-World Cost Example For an app generating 100,000 deep links per month: - Firebase Dynamic Links (if available): $0 (but service discontinued) - Redirectly: $90 ((100,000 - 10,000 free) × $0.001) ## Why Choose Redirectly ### Built for FDL Migrations Redirectly was specifically designed for developers migrating from Firebase Dynamic Links. The SDK API is intuitive and migration typically takes 15-30 minutes. ### Lightweight SDK Only 2-3 MB vs Firebase's 10-15 MB. Minimal impact on app size and startup time. ### Active Maintenance & Support Redirectly is actively maintained and supported. Get timely updates, security patches, and technical assistance. ### Affordable at Scale Transparent pricing without surprise fees. Free tier covers small to medium apps, and scaling costs are predictable. ### Multi-Platform Support Full support for React Native, Flutter, native iOS, and native Android with consistent APIs across all platforms. ## Ready to Switch to Redirectly? Migration from Firebase Dynamic Links takes less than an hour. Start with the [Firebase Dynamic Links Alternative page](https://redirectly.app/firebase-dynamic-links-alternative.md) or the [React Native migration guide](https://redirectly.app/blog/react-native-firebase-dynamic-links-migration.md). ## Related - [Firebase Dynamic Links Shutdown: Timeline & What To Do](https://redirectly.app/blog/firebase-dynamic-links-shutdown.md) — Complete timeline of Firebase Dynamic Links deprecation and impact analysis. - [Migrate from Firebase Dynamic Links in React Native](https://redirectly.app/blog/react-native-firebase-dynamic-links-migration.md) — Step-by-step React Native migration guide with TypeScript code examples. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # How to Set Up Android App Links in Flutter > Implement Android App Links in Flutter with AndroidManifest.xml config, assetlinks.json setup, Dart code, and SHA-256 fingerprint generation. - Canonical: https://redirectly.app/blog/flutter-android-app-links-setup - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-15 - Author: Redirectly - Category: Flutter · 8 min read Master Android App Links with this comprehensive guide. Learn AndroidManifest.xml configuration, assetlinks.json setup, SHA-256 generation, and Dart implementation. ## Table of Contents 1. What are Android App Links? 2. AndroidManifest.xml Configuration 3. Generate SHA-256 Fingerprints 4. Create assetlinks.json File 5. Dart Code Implementation 6. Testing App Links 7. Common Issues & Fixes ## What are Android App Links? **Definition** Android App Links are a special type of deep link that use standard HTTPS URLs to launch your app directly without showing the app chooser dialog. They verify the link belongs to the app domain using the assetlinks.json file hosted on your server. ### Key Advantages - No app chooser dialog - Direct app launching - Domain verification - Seamless UX - Better engagement - Works with Chrome/browsers ### How They Work - User clicks an HTTPS link - Android checks intent filters - Android validates assetlinks.json - App opens if verified - Domain matches SHA-256 - No fallback dialog shown ## AndroidManifest.xml Configuration ### Step 1: Add Intent Filters Update your MainActivity in AndroidManifest.xml to handle App Links: ```xml ``` **Important:** The `android:autoVerify="true"` attribute tells Android to verify this is a valid App Link. ### Step 2: Configure launchMode Set launchMode to singleTask to prevent multiple instances when app links are opened: ```xml android:launchMode="singleTask" ``` ## Generate SHA-256 Fingerprints ### What is SHA-256? A SHA-256 fingerprint is a cryptographic hash of your app's signing certificate. Android uses it to verify that assetlinks.json matches your app. ### Step 1: Generate Debug SHA-256 Use keytool to get the SHA-256 of your debug keystore: ```bash keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android ``` On macOS/Linux. For Windows, replace `~/.android` with your keystore path. ### Step 2: Generate Release SHA-256 For your release keystore (replace paths with your keystore file): ```bash keytool -list -v -keystore /path/to/your/keystore.jks -alias your_key_alias -storepass your_store_password -keypass your_key_password ``` ### Output Format Look for the line starting with "SHA256:". It will look like: `AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99` ## Create assetlinks.json File ### What is assetlinks.json? The assetlinks.json file proves you own both the domain and the app. It must be hosted at `/.well-known/assetlinks.json` on your domain. ### Step 1: Create assetlinks.json Create the JSON file with your app's package name and SHA-256 fingerprints: ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ] } } ] ``` ### Step 2: Multiple Fingerprints (Debug & Release) Include both debug and release SHA-256 fingerprints if you test with both: ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.myapp", "sha256_cert_fingerprints": [ "DEBUG:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB", "RELEASE:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77" ] } } ] ``` ### Step 3: Host on Your Server Upload the file to your domain's .well-known directory: ```text https://yourdomain.com/.well-known/assetlinks.json ``` **Tip:** Validate your assetlinks.json using our [assetlinks Validator](https://redirectly.app/assetlinks-validator.md) tool. ## Dart Code Implementation ### Step 1: Add uni_links Package Add the uni_links package to your pubspec.yaml: ```yaml dependencies: flutter: sdk: flutter uni_links: ^0.9.0 ``` ### Step 2: Create App Link Handler Implement the handler for incoming App Links: ```dart import 'package:flutter/material.dart'; import 'package:uni_links/uni_links.dart'; import 'dart:async'; class AppLinkHandler { static StreamSubscription? _deepLinkSubscription; static Future initialize(BuildContext context) async { // Handle link when app is opened from terminated state final String? initialLink = await getInitialLink(); if (initialLink != null) { _handleAppLink(initialLink, context); } // Listen for app links when app is running _deepLinkSubscription = linkStream.listen( (String link) { _handleAppLink(link, context); }, onError: (err) { debugPrint('App link error: $err'); }, ); } static void _handleAppLink(String link, BuildContext context) { final Uri uri = Uri.parse(link); debugPrint('Received app link: $link'); debugPrint('Scheme: ${uri.scheme}'); debugPrint('Host: ${uri.host}'); debugPrint('Path: ${uri.path}'); debugPrint('Query parameters: ${uri.queryParameters}'); // Route based on path segments if (uri.pathSegments.isNotEmpty) { final String firstSegment = uri.pathSegments[0]; switch (firstSegment) { case 'product': if (uri.pathSegments.length > 1) { final String productId = uri.pathSegments[1]; _navigateToProduct(context, productId); } break; case 'user': if (uri.pathSegments.length > 1) { final String userId = uri.pathSegments[1]; _navigateToUser(context, userId); } break; case 'share': if (uri.pathSegments.length > 1) { final String referralCode = uri.pathSegments[1]; _handleReferral(context, referralCode); } break; default: _navigateToHome(context); } } else { _navigateToHome(context); } } static void _navigateToProduct(BuildContext context, String productId) { debugPrint('Navigate to product: $productId'); // Implementation: Navigate to product screen } static void _navigateToUser(BuildContext context, String userId) { debugPrint('Navigate to user: $userId'); // Implementation: Navigate to user profile screen } static void _handleReferral(BuildContext context, String referralCode) { debugPrint('Handle referral: $referralCode'); // Implementation: Handle referral code } static void _navigateToHome(BuildContext context) { debugPrint('Navigate to home'); // Implementation: Navigate to home screen } static void dispose() { _deepLinkSubscription?.cancel(); } } ``` ### Step 3: Initialize in main.dart Initialize the handler in your main app widget: ```dart import 'package:flutter/material.dart'; import 'app_link_handler.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { AppLinkHandler.initialize(context); }); } @override void dispose() { AppLinkHandler.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( title: 'My App', home: const HomeScreen(), ); } } ``` ## Testing App Links ### Test with adb (Android Debug Bridge) Use ADB to simulate app link clicks: ```bash adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" com.example.myapp ``` ### Manual Testing on Device Test app links manually on your Android device: 1. Open Chrome or another browser on your device 2. Navigate to your app link URL: `https://yourdomain.com/product/123` 3. App should open directly without app chooser dialog 4. Verify correct screen/content is displayed ### Verify assetlinks.json is Live Check if your assetlinks.json is accessible: ```bash curl -I https://yourdomain.com/.well-known/assetlinks.json ``` Expected response: `HTTP/1.1 200 OK` ## Common Issues & Fixes ### Issue: assetlinks.json Not Found (404) **Cause:** File not uploaded or wrong path **Solution:** - Verify file is at `/.well-known/assetlinks.json` - Check file is properly uploaded to server - Test with curl: `curl https://yourdomain.com/.well-known/assetlinks.json` - Validate using our [validator](https://redirectly.app/assetlinks-validator.md) ### Issue: SHA-256 Fingerprint Mismatch **Cause:** Wrong SHA-256 in assetlinks.json **Solution:** - Regenerate SHA-256 using keytool - Ensure fingerprint matches your signing certificate - Check for typos in the hash - Include both debug and release hashes if testing both ### Issue: Deep Link Opens Browser Instead of App **Cause:** Intent filter or autoVerify configuration missing **Solution:** - Verify `android:autoVerify="true"` is set - Check intent filter has correct data entries - Rebuild and reinstall app - Wait a few seconds after install before testing ### Issue: App Not Receiving Deep Link **Cause:** Handler not initialized or context issue **Solution:** - Initialize handler after widget tree is built - Use WidgetsBinding.addPostFrameCallback() - Check debug output for handler logs - Ensure uni_links is in pubspec.yaml ## Master Deep Linking with go_router Now that you have App Links working, learn how to use go_router for advanced deep linking with route guards and deferred links. - [go_router Deep Linking](https://redirectly.app/blog/flutter-deep-linking-go-router.md) - [Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) ## Related Articles - [iOS Universal Links Setup](https://redirectly.app/blog/flutter-universal-links-ios-setup.md) — Learn how to configure Universal Links for iOS with Xcode, AASA file, and implementation. - [Deep Linking with go_router](https://redirectly.app/blog/flutter-deep-linking-go-router.md) — Master go_router for advanced deep linking with route guards and deferred link handling. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # How to Set Up Universal Links in Flutter (iOS) > Set up Universal Links in Flutter for iOS with step-by-step Xcode configuration, AASA file creation, Dart implementation, and troubleshooting. - Canonical: https://redirectly.app/blog/flutter-universal-links-ios-setup - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-10 - Author: Redirectly - Category: Flutter · 8 min read Master Universal Links for iOS with this complete step-by-step guide. Learn Xcode configuration, AASA file setup, and Dart implementation for seamless deep linking. ## Table of Contents 1. What are Universal Links? 2. Xcode Configuration 3. AASA File Setup 4. Dart Code Implementation 5. Testing Universal Links 6. Common Issues & Fixes ## What are Universal Links? **Definition** Universal Links are standard HTTPS URLs that iOS recognizes and routes directly to your app when it's installed. They provide a seamless experience where users never see an app chooser dialog and can be opened on both web and mobile without app-specific URL schemes. ### Key Advantages - No app chooser dialog - Works as web links too - Native iOS integration - Better user experience - SEO friendly - Fallback to web version ### How They Work - User clicks an HTTPS link - iOS checks Associated Domains - iOS validates AASA file - App opens if installed - Fallback to web if not - No manual routing needed ## Xcode Configuration ### Step 1: Add Associated Domains Entitlement Open your Flutter project in Xcode and configure the Associated Domains capability: 1. Open `ios/Runner.xcworkspace` 2. Select the Runner project in the navigator 3. Select the Runner target 4. Go to Signing & Capabilities tab 5. Click + Capability button 6. Search for and add "Associated Domains" ### Step 2: Configure Domain Entries Add your domain to the Associated Domains list: ```text applinks:yourdomain.com applinks:www.yourdomain.com ``` **Note:** Use `applinks:` prefix for Universal Links. You can add multiple domains if needed. ### Step 3: Update Info.plist Ensure your app includes the proper URL handling configuration: ```xml CFBundleURLTypes CFBundleURLName com.example.myapp CFBundleURLSchemes myapp ``` ## AASA File Setup ### What is AASA? The Apple App Site Association (AASA) file is a JSON file that tells iOS which URLs should be handled by your app. It must be hosted at `/.well-known/apple-app-site-association` on your domain. ### Step 1: Create the AASA File Create a JSON file without a file extension: ```json { "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.example.myapp", "paths": [ "/product/*", "/user/*", "/share/*" ] } ] } } ``` ### Step 2: Find Your Team ID You need your Apple Team ID to create the appID. You can find it: 1. Go to [developer.apple.com](https://developer.apple.com) 2. Sign in with your Apple Developer account 3. Go to Account → Membership 4. Copy your Team ID (10-character code) ### Step 3: Upload AASA File Host the file on your web server at the correct path: ```text https://yourdomain.com/.well-known/apple-app-site-association ``` **Tip:** Validate your AASA file using our [AASA Validator](https://redirectly.app/aasa-validator.md) tool to ensure it's correctly formatted. ## Dart Code Implementation ### Step 1: Add uni_links Package Add the uni_links package to your pubspec.yaml: ```yaml dependencies: flutter: sdk: flutter uni_links: ^0.9.0 ``` ### Step 2: Create Deep Link Handler Implement a handler for incoming Universal Links: ```dart import 'package:flutter/material.dart'; import 'package:uni_links/uni_links.dart'; import 'dart:async'; class UniversalLinkHandler { static StreamSubscription? _deepLinkSubscription; static Future initialize(BuildContext context) async { // Handle link when app is opened from terminated state final String? initialLink = await getInitialLink(); if (initialLink != null) { _handleDeepLink(initialLink, context); } // Listen for deep links when app is running _deepLinkSubscription = linkStream.listen( (String link) { _handleDeepLink(link, context); }, onError: (err) { debugPrint('Deep link error: $err'); }, ); } static void _handleDeepLink(String link, BuildContext context) { final Uri uri = Uri.parse(link); debugPrint('Received deep link: $link'); debugPrint('Scheme: ${uri.scheme}'); debugPrint('Host: ${uri.host}'); debugPrint('Path: ${uri.path}'); // Route based on path if (uri.pathSegments.isNotEmpty) { final String firstSegment = uri.pathSegments[0]; switch (firstSegment) { case 'product': if (uri.pathSegments.length > 1) { final String productId = uri.pathSegments[1]; _navigateToProduct(context, productId); } break; case 'user': if (uri.pathSegments.length > 1) { final String userId = uri.pathSegments[1]; _navigateToUser(context, userId); } break; case 'share': if (uri.pathSegments.length > 1) { final String shareCode = uri.pathSegments[1]; _handleShare(context, shareCode); } break; default: _navigateToHome(context); } } } static void _navigateToProduct(BuildContext context, String productId) { debugPrint('Navigate to product: $productId'); // Implementation: Navigate to product screen with productId // Example: Navigator.of(context).pushNamed('/product', arguments: productId); } static void _navigateToUser(BuildContext context, String userId) { debugPrint('Navigate to user: $userId'); // Implementation: Navigate to user profile screen } static void _handleShare(BuildContext context, String shareCode) { debugPrint('Handle share code: $shareCode'); // Implementation: Handle referral or share code } static void _navigateToHome(BuildContext context) { debugPrint('Navigate to home'); // Implementation: Navigate to home screen } static void dispose() { _deepLinkSubscription?.cancel(); } } ``` ### Step 3: Initialize in Main App Call the handler in your main widget: ```dart import 'package:flutter/material.dart'; import 'deep_link_handler.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); // Initialize deep link handler after widget binding WidgetsBinding.instance.addPostFrameCallback((_) { UniversalLinkHandler.initialize(context); }); } @override void dispose() { UniversalLinkHandler.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( title: 'My App', home: const HomeScreen(), ); } } ``` ## Testing Universal Links ### Test on Real Device Testing on a physical iOS device is essential since the simulator has limitations: 1. Build your app to a real iOS device 2. Open Notes or Safari app 3. Type or paste your Universal Link: `https://yourdomain.com/product/123` 4. Long-press the link and tap "Open in App" 5. App should open and navigate to the correct screen ### Using Apple's Validation Tool Check if your AASA file is properly hosted: ```bash curl -I https://yourdomain.com/.well-known/apple-app-site-association ``` Expected response: `HTTP/1.1 200 OK` ### Simulator Limitations The iOS Simulator cannot properly test Universal Links due to sandbox restrictions. Always test on a real device with a valid Apple Developer account and properly signed build. ## Common Issues & Fixes ### Issue: Link Opens in Safari Instead of App **Cause:** AASA file not found or improperly configured **Solution:** - Verify AASA file exists at `/.well-known/apple-app-site-association` - Check AASA JSON syntax using our [validator](https://redirectly.app/aasa-validator.md) - Ensure appID matches your bundle ID and Team ID - Check domain is listed in Associated Domains entitlement ### Issue: Deep Link Not Received by App **Cause:** Deep link handler not properly initialized **Solution:** - Initialize handler in main.dart with proper context - Add debug prints to verify handler is called - Check that uni_links package is properly added - Ensure app is fully launched before testing ### Issue: Associated Domains Entitlement Missing **Cause:** Capability not added in Xcode **Solution:** - Re-open Xcode project - Go to Signing & Capabilities tab - Verify Associated Domains is present - Re-add if missing and rebuild ## Ready for Android? Once you've mastered Universal Links for iOS, implement the Android equivalent using App Links. Learn how to set up App Links in our complete Android guide. - [Android App Links Setup](https://redirectly.app/blog/flutter-android-app-links-setup.md) - [Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) ## Related Articles - [Android App Links Setup](https://redirectly.app/blog/flutter-android-app-links-setup.md) — Learn how to configure App Links for Android with complete setup and Dart implementation guide. - [Deep Linking with go_router](https://redirectly.app/blog/flutter-deep-linking-go-router.md) — Master go_router for advanced deep linking with route guards and deferred link handling. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Linking vs Universal Links vs App Links > Learn the differences between deep linking, Universal Links on iOS, and App Links on Android, plus URI schemes and when to use each one. - Canonical: https://redirectly.app/blog/deep-linking-vs-universal-links-vs-app-links - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2025-03-07 - Author: Dmytro Vyrych - Category: Fundamentals · 8 min read Confused by the terminology? Learn the differences between deep linking, Universal Links (iOS), and App Links (Android). Understand URI schemes, custom protocols, and when to use each approach for seamless app navigation. ## Quick Answer **Deep linking** is the umbrella term. **Universal Links** (iOS) and **App Links** (Android) are Apple and Google's standard implementations of deep linking using standard HTTPS URLs. Custom URI schemes are the older approach. ## Table of Contents - 1. What is Deep Linking? - 2. Custom URI Schemes - 3. Universal Links (iOS) - 4. App Links (Android) - 5. Direct Comparison - 6. When to Use Each ## What is Deep Linking? > **Definition:** Deep linking is the technique of directing users to a specific location within an app, not just opening the app at its home screen. Instead of launching your app's main screen, a deep link takes you directly to the product page, checkout screen, or any specific content. ### Example Flow 1. **User clicks a link** — `https://myapp.com/product/shoes-123` 2. **App detects the deep link** — The URL is parsed to extract product ID: shoes-123 3. **App navigates to destination** — App opens directly to the product page instead of home screen ### Benefits - Better user experience - Higher engagement - Reduced friction - Improved conversion rates - Seamless navigation ### Use Cases - Marketing campaigns - Email campaigns - Social media sharing - Push notifications - In-app referrals ## Custom URI Schemes (The Old Way) ### How URI Schemes Work Custom URI schemes are proprietary protocols registered with the app. When iOS or Android encounters a scheme, it launches the associated app. ```text myapp://product/shoes-123 myapp://user/john-smith myapp://checkout?cart-id=abc123 ``` ### Advantages - Simple to implement - Works on old iOS versions - Works on old Android versions - Custom scheme format - Full control ### Disadvantages - No fallback to web - Shows app chooser dialog - Can be hijacked - Not SEO friendly - Poor user experience > **Why Avoid URI Schemes Today:** Modern apps should use Universal Links (iOS) or App Links (Android). URI schemes have security issues and poor user experience. If someone else registers the same scheme, their app could intercept your links. ## Universal Links (iOS) > **What Are Universal Links?** Universal Links allow iOS apps to be associated with standard HTTPS URLs. When a user taps a link, iOS checks if the app is installed and opens it directly, or falls back to the website if not. ### How Universal Links Work 1. **User taps link** — `https://myapp.com/product/shoes-123` 2. **iOS downloads AASA file** — `https://myapp.com/.well-known/apple-app-site-association` 3. **iOS verifies app ownership** — Checks Team ID and bundle ID match in AASA file 4. **App opens or website loads** — If verified, app opens. If app not installed, website loads in Safari. ### Example AASA File ```json { "applinks": { "apps": [], "details": [ { "appID": "ABC123DEF456.com.myapp.ios", "paths": ["/product/*", "/user/*", "NOT /admin/*"] } ] } } ``` ### Advantages - Standard HTTP/HTTPS URLs - No app chooser dialog - Fallback to website - SEO friendly - Secure and verified ### Disadvantages - Requires AASA file setup - Need domain ownership - iOS caches AASA file - Xcode configuration needed - Only works on iOS [Validate Your AASA File](https://redirectly.app/aasa-validator.md) ## App Links (Android) > **What Are App Links?** App Links are Android's version of Universal Links. They associate standard HTTPS URLs with your app and handle deep linking with automatic fallback to the website if the app isn't installed. ### How App Links Work 1. **User taps link** — `https://myapp.com/product/shoes-123` 2. **Android downloads assetlinks.json** — `https://myapp.com/.well-known/assetlinks.json` 3. **Android verifies package signature** — Confirms app signing certificate matches assetlinks.json 4. **App opens directly (no dialog)** — App opens immediately to specified destination ### Example assetlinks.json ```json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.myapp.android", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ] } } ] ``` ### Advantages - Standard HTTP/HTTPS URLs - No app chooser dialog - Fallback to website - SEO friendly - Secure verification ### Disadvantages - Requires assetlinks.json - Need certificate fingerprint - AndroidManifest.xml setup - Intent filter configuration - Only works on Android [Validate Your assetlinks.json](https://redirectly.app/assetlinks-validator.md) ## Direct Comparison | Aspect | URI Schemes | Universal Links | App Links | | --- | --- | --- | --- | | Platform | iOS & Android | iOS only | Android only | | URL Format | myapp://path | https://myapp.com/path | https://myapp.com/path | | App Chooser Dialog | Yes | No | No | | Fallback to Web | No | Yes | Yes | | SEO Friendly | No | Yes | Yes | | Verification File | None | apple-app-site-association | assetlinks.json | | Recommended | No | Yes | Yes | ## When to Use Each Approach ### URI Schemes - Avoid for New Apps Only use custom URI schemes if you need to support very old iOS or Android versions (pre-2015). They have significant disadvantages and security risks. If you must use them, combine with Universal Links or App Links as a fallback. ### Universal Links - iOS Apps Always use Universal Links for iOS apps. They provide the best user experience, are secure, and work across all modern iOS versions. - Email campaigns - Social media sharing - Marketing links - Push notifications ### App Links - Android Apps App Links are the standard for Android deep linking. They're secure, don't show app chooser dialogs, and support web fallbacks. - Email campaigns - Social media sharing - Marketing links - Push notifications ### Both iOS & Android Apps For cross-platform apps, implement both Universal Links and App Links using the same domain and URL structure. This ensures consistent behavior across both platforms. ## Best Practices - **Use standard HTTPS URLs** — Never use custom URI schemes for new apps - **Implement both platforms** — Support both iOS Universal Links and Android App Links - **Validate your setup** — Test thoroughly on real devices before deploying - **Plan fallback routes** — Ensure web version handles all deep link paths gracefully - **Monitor deep links** — Track which deep links are being used to guide product decisions ## Related Articles - [AASA Validator](https://redirectly.app/aasa-validator.md) — Validate your apple-app-site-association file for iOS Universal Links. - [assetlinks.json Validator](https://redirectly.app/assetlinks-validator.md) — Check your Android App Links configuration with our validator tool. - [Deep Linking Implementation](https://redirectly.app/deep-linking-guide.md) — Step-by-step guide to implementing deep linking in your app. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deferred Deep Link vs Regular Deep Link > Understand the difference between deferred and regular deep links. Learn when to use each, implementation patterns, and user experience trade-offs. - Canonical: https://redirectly.app/blog/deferred-vs-regular-deep-link - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2025-03-07 - Author: Dmytro Vyrych - Category: Fundamentals · 8 min read Master the difference between deferred and regular deep linking. Learn when to use each approach, understand the user flows, see implementation patterns, and discover how they affect user experience and app onboarding. ## Quick Summary **Regular Deep Link:** User clicks a link when the app is already installed. Opens app directly to the specified screen. **Deferred Deep Link:** User clicks a link and doesn't have the app installed yet. After installing from app store, the app remembers the original link and opens to the specified screen on first launch. ## Table of Contents - 1. Regular Deep Linking - 2. Deferred Deep Linking - 3. User Flow Comparison - 4. Implementation Differences - 5. User Experience Differences - 6. When to Use Each - 7. Combined Strategy ## Regular Deep Linking (Installed App) > **What It Is:** Regular deep linking handles users who already have your app installed. When they click a link, the app opens immediately and navigates to the requested screen. ### User Flow 1. **User clicks link** — `https://myapp.com/product/shoes-123` 2. **OS checks if app is installed** — iOS/Android verifies app availability 3. **App launches immediately** — App opens with the deep link data 4. **Navigation handles the link** — App routes user to product screen ### Advantages - Instant app launch - No install delay - Direct navigation - Seamless experience - Immediate engagement ### Limitations - Only works if app installed - Doesn't help new users - Falls back to web if missing - No user acquisition > **Use Case:** Perfect for existing users navigating within your app ecosystem. Examples: email newsletter links, social media posts, internal app referrals, web to app transitions. ## Deferred Deep Linking (App Not Installed) > **What It Is:** Deferred deep linking captures the user's intent before the app is installed. After the user installs the app, the deep link context is delivered on first launch, routing them to the intended destination. ### User Flow 1. **User clicks link** — `https://myapp.com/product/shoes-123` 2. **App not installed detected** — System redirects to app store 3. **Link context stored** — Platform service remembers the deep link intent 4. **User installs app** — User downloads and installs from store 5. **First launch delivers context** — App queries for stored deep link on first open 6. **Deferred navigation happens** — User sees product page instead of home screen ### Advantages - Works for new users - Boosts app discovery - Better onboarding - Increases conversions - Improves engagement - Tracks user intent ### Considerations - Depends on backend service - Requires SDK integration - Extra complexity - Privacy considerations - Potential costs > **Use Case:** Essential for user acquisition. Examples: paid ad campaigns, influencer links, organic search results, viral sharing campaigns where you expect many new users. ## Side-by-Side Flow Comparison ### Regular Deep Link 1. User clicks link 2. App already installed 3. App launches 4. Navigate to screen ### Deferred Deep Link 1. User clicks link 2. App not installed 3. Store link context 4. User installs app 5. First open 6. Navigate to screen ## Implementation Differences ### Regular Deep Link Implementation Handled entirely by the app using native iOS/Android linking APIs or cross-platform frameworks like React Navigation. ```typescript // React Native Example const linking = { prefixes: ['https://myapp.com'], config: { screens: { Product: 'product/:id', }, }, }; ``` App automatically parses incoming URL and routes user. No backend service needed. ### Deferred Deep Link Implementation Requires a backend service to store link context and deliver it on first app open. Typically uses deep linking SDKs from platforms like Branch, AppsFlyer, or Adjust. ```typescript // Branch SDK Example import { Branch } from 'react-native-branch'; // Initialize Branch.subscribe(({ error, params }) => { if (error) console.log('Error: ' + error); // params contain the deep link data if (params['+clicked_branch_link']) { // Handle deferred deep link } }); ``` SDK queries backend service on first launch to retrieve stored link context. > **Architecture Difference:** Regular deep linking is a direct link: URL → App → Navigation. Deferred deep linking adds a service layer: URL → Service (stores context) → App Store → First Open (retrieve context from service) → Navigation. ## User Experience Differences ### Regular Deep Link UX - **Timeline** — Click → Instant App Open (< 1 second) - **User Perception** — Seamless, fast, no interruption - **Friction Points** — None - direct app launch - **Best For** — Engaged users who already have the app ### Deferred Deep Link UX - **Timeline** — Click → App Store → Download (5-60 seconds) → First Open → Navigation - **User Perception** — Install required, but still gets intended experience after - **Friction Points** — App store redirect, install delay, but rewarded with contextual landing - **Best For** — New users discovering app through marketing > **Key Insight:** Deferred deep linking turns an install into a directed onboarding experience. Instead of showing new users your home screen, you can introduce them directly to the product they showed interest in. ## When to Use Each Approach ### Use Regular Deep Linking When - Users already have your app installed - You want instant navigation with zero friction - Links are for existing users (email, newsletters) - You don't need install attribution - Keeping things simple and lightweight ### Use Deferred Deep Linking When - Targeting new users through marketing campaigns - You need install attribution tracking - You want to optimize first-launch onboarding - You're running paid ad campaigns - You want to track campaign ROI ## Best Practice: Use Both Together ### Combined Strategy The most effective approach uses both regular and deferred deep linking in a single implementation: 1. **Native Deep Linking (Always)** — Implement regular deep linking for all your users. This handles existing users seamlessly. 2. **Deferred Deep Linking (For Marketing)** — For paid campaigns and new user acquisition, layer on deferred deep linking to capture new users. 3. **Fallback to Web** — Always provide a web fallback for users who don't install or for verification purposes. **Result:** 100% of users get the intended experience—installed users get instant navigation, new users get guided onboarding after install. ## Key Takeaways 1. **Regular deep linking** is for installed apps—instant, seamless, zero friction 2. **Deferred deep linking** captures new user intent and delivers it after install 3. Regular = instant navigation, Deferred = new user onboarding 4. Use both together for complete coverage of all user scenarios 5. Deferred deep linking requires backend service; regular is just app routing ## Related Articles - [Complete Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) — Master all aspects of deep linking: implementation, best practices, and testing strategies. - [Deep Linking vs Universal Links vs App Links](https://redirectly.app/blog/deep-linking-vs-universal-links-vs-app-links.md) — Understand the differences between URI schemes, Universal Links, and App Links. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Debugging Deep Links in Flutter: Complete Troubleshooting Guide > Fix Universal Links, App Links, and deferred deep links in Flutter. A step-by-step debugging guide with solutions for the most common issues. - Canonical: https://redirectly.app/blog/debugging-deep-links-flutter - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2026-03-07 - Author: Redirectly - Category: Flutter · 8 min read Deep links are critical for user experience, but they're notoriously difficult to debug when things go wrong. This comprehensive guide walks you through systematically diagnosing and fixing deep linking issues in Flutter apps. ## Why Deep Links Break Deep links often fail silently, meaning your app receives no error messages when a link doesn't work. The user just gets sent to the home screen instead of the intended deep-linked screen. This happens because deep linking involves multiple systems: - **iOS:** Apple's AASA (Apple App Site Association) file, Xcode entitlements, and Safari handling - **Android:** assetlinks.json file, intent filters, and app signing certificates - **Your App:** Proper route handling and link parsing in your Flutter code When any part of this chain breaks, the entire deep link fails. Let's walk through how to systematically debug each component. ## #1 Universal Links Not Working (iOS) > **iOS Requires HTTPS:** Universal Links only work with HTTPS domains. HTTP will not work, even during testing. ### Step 1: Validate Your AASA File The Apple App Site Association (AASA) file is the foundation of iOS Universal Links. Use our validator to ensure it's correctly formatted: [Open AASA Validator](https://redirectly.app/aasa-validator.md) ### Step 2: Check Associated Domains in Xcode Your Xcode project must have the Associated Domains entitlement configured: ```xml com.apple.developer.associated-domains applinks:example.com applinks:www.example.com ``` To do this in Xcode: Select your target → Signing & Capabilities → Add Capability → Associated Domains → Add your domain with the `applinks:` prefix. ### Step 3: Verify AASA File Hosting Your AASA file must be hosted at exactly: `https://example.com/.well-known/apple-app-site-association` ```bash # Test if your AASA file is accessible (replace example.com) curl -v https://example.com/.well-known/apple-app-site-association ``` Check the response headers. The `Content-Type` must be: ```text Content-Type: application/json ``` ### Step 4: Test on a Real Device iOS Simulator does NOT validate AASA files or properly handle Universal Links. You must test on a real device: ```bash # Install your app on a real device # Then test the link in Safari # For real device: # 1. Open Safari # 2. Paste your deep link URL # 3. Tap the link # 4. Your app should launch with the route ``` ### Step 5: Check AASA File Caching iOS aggressively caches AASA files. After updating your AASA file: 1. Uninstall the app completely from the device 2. Restart the device 3. Reinstall the app 4. Test the deep link again > **iOS Caching Gotcha:** If your deep link worked once but now doesn't after updating AASA, it's likely a cache issue. Force refresh by fully uninstalling and reinstalling. ## #2 App Links Not Working (Android) Android App Links require the assetlinks.json file, correct intent filters, and matching app signatures. Let's go through each: ### Step 1: Validate assetlinks.json Use our validator to ensure your assetlinks.json is correctly formatted: [Open assetlinks.json Validator](https://redirectly.app/assetlinks-validator.md) ### Step 2: Get Your App's SHA-256 Fingerprint Your assetlinks.json needs the SHA-256 fingerprint of your signing certificate. Get it differently for debug vs. release: **For Debug Key:** ```bash keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep "SHA1" ``` **For Release Key:** ```bash keytool -list -v -keystore /path/to/your/keystore.jks -alias your-alias ``` > **Critical: Debug vs Release Keys:** Your debug and release keys have different SHA-256 fingerprints. If you test with the debug APK but your assetlinks.json has the release fingerprint, deep links won't work. Use the correct fingerprint for each build type. ### Step 3: Configure AndroidManifest.xml Your intent filter must have `autoVerify="true"`: ```xml ``` ### Step 4: Host assetlinks.json Your assetlinks.json must be hosted at: `https://example.com/.well-known/assetlinks.json` ```bash curl -v https://example.com/.well-known/assetlinks.json ``` ### Step 5: Test with adb Test deep links directly using Android Debug Bridge: ```bash adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/deep-link" com.your.package ``` If the intent filter matches correctly, you'll see the app launch. Check Logcat for any errors: ```bash adb logcat | grep "your-package-name" ``` ## #3 Deferred Deep Links Not Firing After Install Deferred deep links (links that fire after app installation) have additional complexity. Here's how to debug: ### Understanding the Attribution Window Deferred deep links work within an "attribution window" (typically 24-48 hours). If the user installs the app after the attribution window closes, the deferred link won't fire. You can: - Extend the attribution window in your Redirectly dashboard - Use server-side link resolution for longer-lived attribution ### Step 1: Verify SDK Initialization Your Redirectly SDK must be initialized early in your app lifecycle, before checking for deep links: ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Redirectly FIRST await Redirectly.initialize(apiKey: "YOUR_API_KEY"); // Then check for deferred links await Redirectly.checkDeepLink((deepLink) { // Handle the deferred deep link print("Deferred link: ${deepLink.url}"); }); runApp(MyApp()); } ``` ### Step 2: Check Your API Key Deferred deep links require a valid API key. Verify it's correct in your Flutter code: 1. Go to your [Redirectly Dashboard](https://dashboard.redirectly.app) 2. Navigate to Settings → API Keys 3. Copy your production API key 4. Paste it in your `Redirectly.initialize()` call ### Step 3: Verify Link Was Created with Deferred=True Your link must be created with the deferred parameter set. Check in your dashboard: 1. Go to your Redirectly Dashboard 2. Find the link you created 3. Verify that "Deferred Deep Linking" is enabled 4. Check that the link hasn't expired (past the attribution window) ### Step 4: Debug with Redirectly Dashboard The Redirectly dashboard shows real-time attribution data: 1. Create a test deferred link 2. Open your app's link on a new device 3. Uninstall the app on that device 4. Install it again within the attribution window 5. Check the dashboard link analytics to see if the device was attributed > **Testing Deferred Links Locally:** To test deferred links on your computer, use Redirectly's test endpoint or create a temporary link and immediately uninstall/reinstall your debug app on a connected device. ## #4 Testing Deep Links ### iOS Simulator Limitations The iOS Simulator has limited deep link support. Use the command: ```bash xcrun simctl openurl booted "https://example.com/deep-link" ``` However, this doesn't validate AASA files or truly test Universal Links. For accurate testing, you must use a real device. ### Android Emulator Testing Android Emulator supports deep link testing better than iOS Simulator: ```bash adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/deep-link" com.your.package ``` ### Real Device Testing Workflow 1. **iOS:** Open Safari → Paste your deep link URL → Tap the link → Verify your app launches to the correct screen 2. **Android:** Use adb with the command above, or open your browser → Paste link → Tap → Verify 3. Capture logs: Check logcat (Android) or Xcode console (iOS) for any errors 4. Verify route handling: Confirm your Flutter app correctly parses and navigates to the intended route ### Using Redirectly Dashboard to Verify Redirectly's dashboard shows link click analytics in real-time: 1. Create a test link in your dashboard 2. Click the link from your device browser 3. Check if it opens your app 4. Check the dashboard to see if the click was recorded ## #5 Common Issues Checklist Go through this checklist systematically when debugging deep links: - [ ] **AASA/assetlinks.json file exists and is valid JSON** — Use our validators to check - [ ] **Files are hosted at correct URLs (.well-known/)** — Test with curl to verify accessibility - [ ] **iOS: Associated Domains entitlement added in Xcode** — With applinks: prefix, HTTPS domain only - [ ] **Android: Intent filter has autoVerify="true"** — In AndroidManifest.xml for App Links to work - [ ] **Android: SHA-256 fingerprint matches your signing key** — Debug key and release key have different fingerprints - [ ] **iOS: Using HTTPS (not HTTP) domain** — Universal Links require HTTPS - [ ] **iOS: Content-Type header is application/json** — Verify with curl -v - [ ] **Flutter app correctly handles deep link routes** — Route paths must match your link structure - [ ] **Deferred links: Redirectly SDK initialized at app startup** — Before any route handling logic - [ ] **Testing on real devices (not simulators/emulators)** — Simulators don't fully support Universal Links ## Next Steps Deep link debugging requires methodically checking each layer of the system. Start with validating your AASA and assetlinks.json files, then verify your Xcode and Android configurations, and finally test thoroughly on real devices. If you're still having issues, use these resources to diagnose further: - [AASA Validator](https://redirectly.app/aasa-validator.md) — Validate your Apple App Site Association file - [assetlinks.json Validator](https://redirectly.app/assetlinks-validator.md) — Validate your Android assetlinks.json file - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Complete Flutter deferred deep linking guide - [More Resources](https://redirectly.app/blog.md) — Explore our complete deep linking guides --- Full site index for AI agents: https://redirectly.app/llms.txt --- # How to Migrate from Firebase Dynamic Links to Redirectly > Step-by-step guide to migrating your Flutter app from Firebase Dynamic Links to Redirectly. Keep your deep links working with our drop-in SDK. - Canonical: https://redirectly.app/blog/migrating-from-firebase-dynamic-links - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-02-15 - Author: Dmytro Vyrych - Category: Migration · 10 min read With Firebase Dynamic Links (FDL) shutting down, mobile developers need a reliable alternative. Here is a complete guide to switching your Flutter app to Redirectly with minimal code changes. ## Why Migrate? Google announced the deprecation of Firebase Dynamic Links, leaving many developers searching for a replacement. Redirectly was built specifically to fill this gap, offering a similar "deferred deep linking" capability but with a focus on the Flutter ecosystem. See our full comparison: [Firebase Dynamic Links Alternative](https://redirectly.app/firebase-dynamic-links-alternative.md). The migration process involves three main steps: 1. Setting up your project in Redirectly 2. Configuring your domain and native platforms 3. Replacing the Flutter SDK code ### Step 1: Create Your Redirectly Project First, sign up for a free account on the [Redirectly Dashboard](https://dashboard.redirectly.app/dashboard). Create a new project and note down your **API Key**. You will need this to initialize the SDK. ### Step 2: Replace Dependencies Remove the `firebase_dynamic_links` package from your `pubspec.yaml` and add `flutter_redirectly`. ```diff dependencies: - firebase_dynamic_links: ^5.4.0 + flutter_redirectly: ^1.0.0 ``` ### Step 3: Initialize the SDK In your `main.dart`, replace the Firebase initialization with Redirectly. For detailed implementation instructions, check out our [Flutter Deferred Deep Linking guide](https://redirectly.app/flutter-deferred-deep-linking.md) and [React Native Deferred Deep Linking guide](https://redirectly.app/react-native-deferred-deep-linking.md). ```dart // OLD: Firebase await Firebase.initializeApp(); // NEW: Redirectly final redirectly = FlutterRedirectly(); await redirectly.initialize( RedirectlyConfig( apiKey: 'YOUR_API_KEY', enableDebugLogging: kDebugMode, ), ); ``` ### Step 4: Handle Incoming Links The API for listening to links is very similar. Redirectly provides a stream for foreground clicks and a method for initial links. #### Foreground Listeners ```dart // OLD: Firebase FirebaseDynamicLinks.instance.onLink.listen((dynamicLinkData) { final Uri deepLink = dynamicLinkData.link; // Handle link }); // NEW: Redirectly redirectly.onLinkClick.listen((linkClick) { final Uri deepLink = linkClick.originalUrl; // or linkClick.link.target // Handle link }); ``` #### Initial Link (App Launch) ```dart // OLD: Firebase final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink(); // NEW: Redirectly final LinkClick? initialLink = await redirectly.getInitialLink(); ``` ### Step 5: Update AndroidManifest & Info.plist Just like with FDL, you need to register your domain for Android App Links and iOS Universal Links. - **Android:** Update your `intent-filter` host to match your Redirectly custom domain (e.g., `app.redirectly.app` or your own domain). - **iOS:** Update your `Associated Domains` entitlement to include your new domain. ### Step 6: Creating Links If you generate links programmatically, switch to the Redirectly API: ```dart // NEW: Redirectly final link = await redirectly.createLink( slug: 'promo-2024', target: 'https://myapp.com/promo', metadata: { 'campaign': 'spring_sale', 'source': 'instagram' } ); print(link.shortUrl); // https://your.domain/promo-2024 ``` ## Conclusion Migrating to Redirectly gives you full control over your deep links with a modern, supported platform. If you have any questions during migration, check out our [documentation](https://docs.redirectly.app) or [start building free deep links today](https://redirectly.app/index.md). --- Full site index for AI agents: https://redirectly.app/llms.txt --- # What is Deferred Deep Linking? The Complete Guide > Learn how deferred deep linking solves the attribution problem in mobile app marketing and user acquisition. - Canonical: https://redirectly.app/blog/what-is-deferred-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-22 - Author: Redirectly - Category: Fundamentals · 6 min read Learn how deferred deep linking solves the attribution problem in mobile app marketing. Understand the difference between regular deep linking and deferred deep linking, and when to use each. ## What is Deferred Deep Linking? **Definition:** Deferred deep linking is a technology that allows users to click a link, install an app, and then be taken to the specific content they originally intended to access - even if they didn't have the app installed when they first clicked the link. It "defers" the deep link action until after the app installation is complete. **Regular Deep Linking** — Only works if the app is already installed on the device. - User clicks link → App opens (if installed) - Fails if app not installed **Deferred Deep Linking** — Works whether the app is installed or not, with perfect attribution. - User clicks link → Install app → Navigate to content - Always works with attribution ## The Attribution Problem ### The Challenge Traditional deep linking has a fundamental limitation: it only works when the app is already installed. This creates a significant problem for mobile app marketing and user acquisition. 1. User clicks a marketing link for a product in your app 2. App isn't installed, so they're redirected to app store 3. User installs app and opens it 4. **Problem:** App doesn't know which product they wanted to see! ### Business Impact **Lost Opportunities:** - Users can't find the content they clicked for - Poor first-time user experience - Higher app uninstall rates - Lost marketing attribution - Inability to measure campaign effectiveness **Marketing Challenges:** - Can't track which campaigns drive installs - No way to measure ROI on marketing spend - Difficulty optimizing ad campaigns - Lost referral program attribution - Incomplete user journey analytics ## How Deferred Deep Linking Works ### The Deferred Deep Linking Process 1. **User Clicks Link** — User clicks a marketing link (e.g., from email, social media, or ad) 2. **Attribution Tracking** — System creates a unique fingerprint of the user's device and stores the intended destination 3. **App Store Redirect** — User is redirected to app store to install the app 4. **App Installation** — User installs and opens the app for the first time 5. **Attribution Match** — App checks for matching device fingerprint and retrieves the original intent 6. **Deferred Navigation** — User is taken to the specific content they originally intended to access ### Device Fingerprinting The key to deferred deep linking is creating a unique "fingerprint" of the user's device that can be matched after installation: **Fingerprint Data:** - Device model and OS version - Screen resolution - Timezone and language - IP address (anonymized) - Browser/User Agent **Stored Intent:** - Original deep link URL - Campaign parameters - User context - Timestamp - Expiration time ## Deferred vs Regular Deep Linking | Feature | Regular Deep Linking | Deferred Deep Linking | | --- | --- | --- | | App Installation Required | Yes | No | | Marketing Attribution | Limited | Complete | | User Experience | Broken if app not installed | Seamless always | | Implementation Complexity | Simple | Moderate | | Use Case | Existing users | New user acquisition | **When to Use Regular Deep Linking:** - Push notifications to existing users - In-app sharing between users - Cross-app communication - Simple navigation within app - Quick prototyping and testing **When to Use Deferred Deep Linking:** - Marketing campaigns and ads - Email marketing - Social media sharing - Referral programs - Influencer partnerships - Content marketing ## Use Cases & Benefits ### Primary Use Cases - **Email Marketing** — Send users directly to specific products or features in your app - **Social Media** — Share content that opens directly in your app, even for new users - **Paid Advertising** — Track which ads drive the most valuable app installs - **Referral Programs** — Reward users for successful referrals with perfect attribution - **Content Marketing** — Drive traffic from blog posts and articles to specific app content - **Influencer Partnerships** — Track and measure influencer campaign effectiveness **Business Benefits:** - Complete marketing attribution - Higher conversion rates - Better user onboarding - Improved campaign ROI - Enhanced user retention - Data-driven marketing decisions **User Experience Benefits:** - Seamless app installation flow - Immediate access to desired content - No manual navigation required - Consistent experience across platforms - Reduced friction in user journey - Higher satisfaction and engagement ## Implementation Overview ### Implementation Approaches **Build Your Own** — Create a custom deferred deep linking solution: - Full control over implementation - Custom fingerprinting algorithms - Complex to build and maintain - Requires significant development time **Use a Service** — Leverage existing deferred deep linking platforms: - Quick implementation - Proven reliability and accuracy - Built-in analytics and reporting - Ongoing maintenance and updates ### Key Implementation Components 1. **Device Fingerprinting** — Create unique device identifiers using hardware and software characteristics 2. **Attribution Storage** — Store user intent and campaign data with device fingerprints 3. **App Integration** — SDK integration to check for deferred deep links on app launch 4. **Navigation Logic** — Parse stored intent and navigate to the correct app screen ### Flutter Integration Example Here's a simplified example of how deferred deep linking works in Flutter. For detailed implementation instructions, see the [Flutter Deferred Deep Linking guide](https://redirectly.app/flutter-deferred-deep-linking.md). A [React Native guide](https://redirectly.app/react-native-deferred-deep-linking.md) is also available. ```dart import 'package:flutter_redirectly/flutter_redirectly.dart'; class DeferredDeepLinkService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static Future initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for app install events (deferred deep linking) _redirectly.onAppInstalled.listen(_handleDeferredDeepLink); } static void _handleDeferredDeepLink(AppInstallEvent event) { if (event.matched) { // This user came from a deferred deep link! print('Deferred deep link matched!'); print('Original link: ${event.link}'); print('Username: ${event.username}'); print('Slug: ${event.slug}'); // Navigate to the intended content _navigateToIntendedContent(event); } else { // This is an organic install print('Organic install detected'); } } static void _navigateToIntendedContent(AppInstallEvent event) { // Parse the original link and navigate accordingly final uri = Uri.parse(event.link!); if (uri.path.contains('/product/')) { final productId = uri.path.split('/').last; // Navigate to product page _navigateToProduct(productId); } else if (uri.path.contains('/user/')) { final userId = uri.path.split('/').last; // Navigate to user profile _navigateToUser(userId); } } } ``` ## Unlock the Power of Deferred Deep Linking Deferred deep linking is essential for modern mobile app marketing. It solves the attribution problem and provides seamless user experiences that drive higher conversion rates and better user engagement. Learn more about deferred deep linking implementation in the [Deferred Deep Linking guide](https://redirectly.app/deferred-deep-linking.md). ## Related - [What is Deep Linking?](https://redirectly.app/blog/what-is-deep-linking.md) — Learn the fundamentals of deep linking and how it works in mobile app development. - [Marketing Campaigns with Deep Linking](https://redirectly.app/blog/marketing-campaigns-deep-linking.md) — See how to implement marketing campaigns using deferred deep linking for maximum ROI. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # What is Deep Linking? A Complete Guide for Developers > Learn the fundamentals of deep linking, how it works, and why it's essential for modern mobile app development. - Canonical: https://redirectly.app/blog/what-is-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-20 - Author: Dmytro Vyrych - Category: Fundamentals · 5 min read Learn the fundamentals of deep linking, how it works, and why it's essential for modern mobile app development. Understand the difference between deep links, universal links, and app links. ## What is Deep Linking? **Definition:** Deep linking is a technology that allows users to navigate directly to specific content or functionality within a mobile app through a URL, rather than just opening the app's main screen. It creates a bridge between web content and mobile app content, enabling seamless user experiences across platforms. **Traditional App Launch** — Without deep linking, clicking a link only opens the app's home screen, requiring users to manually navigate to the desired content. ```text myapp:// → Opens app home screen ``` **Deep Link Launch** — With deep linking, users are taken directly to the specific content they intended to access. ```text myapp://product/123 → Opens specific product page ``` ## How Deep Linking Works ### The Deep Linking Process 1. **User Clicks Link** — User clicks a deep link from email, web page, or another app 2. **System Identifies App** — Operating system recognizes the URL scheme and identifies the target app 3. **App Launches** — Target app launches and receives the deep link URL 4. **Content Navigation** — App parses the URL and navigates to the specific content or screen ### Example Deep Link Flow - Link: `https://myapp.com/product/123` - Parsed: `Product ID: 123` - Result: User sees product #123 details page ## Types of Deep Links ### 1. Custom URL Schemes Custom URL schemes use a unique protocol identifier to launch your app directly. - Format: `myapp://path/to/content` - Example: `myapp://product/123` **Note:** Custom schemes only work if the app is installed. They don't provide fallback options. ### 2. Universal Links (iOS) Universal Links use standard HTTPS URLs that work on both web and mobile, with intelligent routing. To verify your iOS Universal Links configuration, use the [AASA Validator tool](https://redirectly.app/aasa-validator.md). - Format: `https://myapp.com/path` - Example: `https://myapp.com/product/123` **Benefit:** If app isn't installed, opens web page. If installed, opens app directly. ### 3. App Links (Android) App Links are Android's equivalent to Universal Links, providing seamless web-to-app experiences. Validate your Android App Links configuration using the [Asset Links Validator](https://redirectly.app/assetlinks-validator.md). - Format: `https://myapp.com/path` - Example: `https://myapp.com/product/123` **Benefit:** Verified links that bypass the app chooser dialog for better UX. ## Benefits & Use Cases **Key Benefits:** - Seamless user experience - Reduced friction in navigation - Better user engagement - Improved conversion rates - Enhanced app discoverability - Cross-platform consistency **Common Use Cases:** - Email marketing campaigns - Social media sharing - Push notifications - QR code scanning - Web-to-app transitions - App-to-app communication ### Real-World Examples - **E-commerce App** — Email promotion for a specific product: `https://shop.app/product/sale-item-123` - **Social Media App** — Share a specific post or profile: `https://social.app/post/abc123` - **News App** — Direct link to an article: `https://news.app/article/breaking-news` - **Fitness App** — Link to a specific workout: `https://fitness.app/workout/beginner-yoga` ## Basic Implementation ### 1. URL Scheme Registration Register your app's URL scheme in the platform-specific configuration files: iOS (Info.plist): ```xml CFBundleURLTypes CFBundleURLName com.myapp.deeplink CFBundleURLSchemes myapp ``` Android (AndroidManifest.xml): ```xml ``` ### 2. Flutter Implementation Handle incoming deep links in your Flutter app: ```dart import 'package:flutter/material.dart'; import 'package:uni_links/uni_links.dart'; class DeepLinkHandler { static void initialize() { // Handle app launch from deep link getInitialLink().then((String? link) { if (link != null) { _handleDeepLink(link); } }); // Handle deep links when app is running linkStream.listen((String link) { _handleDeepLink(link); }); } static void _handleDeepLink(String link) { final uri = Uri.parse(link); switch (uri.scheme) { case 'myapp': _handleCustomScheme(uri); break; case 'https': _handleUniversalLink(uri); break; } } static void _handleCustomScheme(Uri uri) { final path = uri.path; if (path.startsWith('/product/')) { final productId = path.split('/').last; // Navigate to product page _navigateToProduct(productId); } else if (path.startsWith('/user/')) { final userId = path.split('/').last; // Navigate to user profile _navigateToUser(userId); } } static void _handleUniversalLink(Uri uri) { // Handle universal links (same logic as custom schemes) _handleCustomScheme(uri); } static void _navigateToProduct(String productId) { // Navigation logic here print('Navigate to product: $productId'); } static void _navigateToUser(String userId) { // Navigation logic here print('Navigate to user: $userId'); } } ``` ## Best Practices **URL Design:** - Use clear, hierarchical paths - Keep URLs short and memorable - Use consistent naming conventions - Include version information if needed - Handle missing parameters gracefully - Validate all input parameters **Error Handling:** - Provide fallback navigation - Show user-friendly error messages - Log deep link failures for debugging - Handle malformed URLs gracefully - Test edge cases thoroughly - Implement retry mechanisms **Pro Tips:** - Always test deep links on both iOS and Android devices - Use universal links/app links for better user experience - Implement proper analytics to track deep link usage - Consider using a deep linking service for complex scenarios - Document your deep link structure for team members - Plan for future URL structure changes with versioning ## Master Deep Linking Today Deep linking is a fundamental technology for modern mobile apps. By implementing it correctly, you can create seamless user experiences that bridge the gap between web and mobile platforms. For a comprehensive deep linking guide, check out the [Complete Deep Linking Guide](https://redirectly.app/deep-linking-guide.md). ## Related - [What is Deferred Deep Linking?](https://redirectly.app/blog/what-is-deferred-deep-linking.md) — Learn about deferred deep linking and how it solves the app installation attribution problem. - [Email Verification with Deep Linking](https://redirectly.app/blog/email-verification-deep-linking.md) — See how to implement secure email verification using deep linking in your Flutter app. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Email Verification with Deep Linking: Complete Implementation Guide > Learn how to implement secure email verification flows using deep links, with a step-by-step guide and code examples for mobile and web apps. - Canonical: https://redirectly.app/blog/email-verification-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-15 - Author: Dmytro Vyrych - Category: Authentication · 8 min read Learn how to implement secure email verification using temporary deep links that work seamlessly across web and mobile platforms. This comprehensive guide covers everything from backend setup to Flutter integration. ## Table of Contents - 1. Overview & Benefits - 2. System Architecture - 3. Backend Implementation - 4. Flutter Integration - 5. Security Best Practices - 6. Testing & Validation ## Overview & Benefits > **What is Email Verification with Deep Linking?** Email verification with deep linking allows users to verify their email addresses by clicking a link that opens directly in your app, providing a seamless experience across web and mobile platforms. Instead of traditional web-only verification, users get a native app experience with proper navigation and state management. ### Key Benefits - Seamless cross-platform experience - Higher verification completion rates - Better user engagement and retention - Secure token-based verification - Automatic expiration for security - Detailed analytics and tracking ### Use Cases - User account registration - Email address changes - Two-factor authentication setup - Newsletter subscriptions - Account recovery processes - Marketing campaign opt-ins ## System Architecture ### How It Works 1. **User Registration** — User signs up with email address in your Flutter app 2. **Verification Link Creation** — Backend creates temporary verification link using Redirectly API 3. **Email Delivery** — Verification link sent to user's email address 4. **Link Click & Verification** — User clicks link, app opens, email gets verified ## Backend Implementation ### 1. User Registration Endpoint First, create an endpoint that handles user registration and triggers email verification: ```javascript // Node.js/Express example const axios = require('axios'); const crypto = require('crypto'); // Initialize Redirectly client const redirectly = axios.create({ baseURL: 'https://redirectly.app/api', headers: { 'Authorization': `Bearer ${process.env.REDIRECTLY_API_KEY}`, 'Content-Type': 'application/json' } }); app.post('/api/register', async (req, res) => { try { const { email, password, username } = req.body; // 1. Create user account (unverified) const user = await createUser({ email, password: await hashPassword(password), username, email_verified: false }); // 2. Generate verification token const verificationToken = crypto.randomBytes(32).toString('hex'); await storeVerificationToken(user.id, verificationToken); // 3. Create temporary verification link const verificationLink = await createVerificationLink(user.id, verificationToken); // 4. Send verification email await sendVerificationEmail(email, verificationLink); res.json({ success: true, message: 'Registration successful. Please check your email to verify your account.', user: { id: user.id, email, username } }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'Registration failed' }); } }); ``` ### 2. Verification Link Creation Create a function that generates temporary verification links using Redirectly: ```javascript async function createVerificationLink(userId, token) { try { // Create temporary link that expires in 24 hours const response = await redirectly.post('/v1/temp-links', { target: `https://redirectly.app/verify-email?token=${token}&user=${userId}`, ttlSeconds: 86400 // 24 hours }); return response.data.url; } catch (error) { console.error('Failed to create verification link:', error); throw new Error('Failed to create verification link'); } } async function storeVerificationToken(userId, token) { // Store token in database with expiration await db.query( 'INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES (?, ?, ?)', [userId, token, new Date(Date.now() + 24 * 60 * 60 * 1000)] ); } ``` ### 3. Email Verification Handler Create an endpoint that handles the verification when the link is clicked: ```javascript app.get('/api/verify-email', async (req, res) => { try { const { token, user } = req.query; // 1. Validate token const tokenRecord = await db.query( 'SELECT * FROM email_verification_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW()', [token, user] ); if (!tokenRecord.length) { return res.status(400).json({ error: 'Invalid or expired verification token' }); } // 2. Mark email as verified await db.query( 'UPDATE users SET email_verified = true WHERE id = ?', [user] ); // 3. Delete used token await db.query( 'DELETE FROM email_verification_tokens WHERE token = ?', [token] ); // 4. Return success response res.json({ success: true, message: 'Email verified successfully' }); } catch (error) { console.error('Email verification error:', error); res.status(500).json({ error: 'Verification failed' }); } }); ``` ## Flutter Integration ### 1. Setup Deep Link Handling Configure your Flutter app to handle incoming verification links: ```dart import 'package:flutter_redirectly/flutter_redirectly.dart'; class EmailVerificationService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static Future initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for incoming links _redirectly.onLinkClick.listen(_handleIncomingLink); // Check for initial link (app opened from link) final initialLink = await _redirectly.getInitialLink(); if (initialLink != null) { _handleIncomingLink(initialLink); } } static void _handleIncomingLink(LinkClickEvent event) { if (event.error != null) { print('Link error: ${event.error}'); return; } final uri = Uri.parse(event.originalUrl); // Check if this is an email verification link if (uri.path.contains('/verify-email')) { final token = uri.queryParameters['token']; final userId = uri.queryParameters['user']; if (token != null && userId != null) { _processEmailVerification(token, userId); } } } static Future _processEmailVerification(String token, String userId) async { try { // Call your backend to verify the email final response = await http.get( Uri.parse('${ApiConfig.baseUrl}/api/verify-email?token=$token&user=$userId'), headers: {'Content-Type': 'application/json'}, ); if (response.statusCode == 200) { // Show success message _showVerificationSuccess(); // Update app state await AuthService.updateEmailVerificationStatus(true); // Navigate to main app Get.offAllNamed('/home'); } else { _showVerificationError('Verification failed. Please try again.'); } } catch (e) { _showVerificationError('Network error. Please check your connection.'); } } static void _showVerificationSuccess() { Get.snackbar( 'Success', 'Your email has been verified successfully!', backgroundColor: Colors.green, colorText: Colors.white, duration: Duration(seconds: 3), ); } static void _showVerificationError(String message) { Get.snackbar( 'Error', message, backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 5), ); } } ``` ### 2. Initialize in main.dart Initialize the email verification service when your app starts: ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize email verification service await EmailVerificationService.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'My App', theme: ThemeData( primarySwatch: Colors.blue, ), home: AuthService.isLoggedIn() ? HomeScreen() : LoginScreen(), routes: { '/home': (context) => HomeScreen(), '/login': (context) => LoginScreen(), '/verify-email': (context) => EmailVerificationScreen(), }, ); } } ``` ## Security Best Practices **Critical Security Measures:** - Use cryptographically secure random tokens (32+ bytes) - Set appropriate expiration times (24 hours max for email verification) - Implement rate limiting on verification endpoints - Use HTTPS for all verification links - Validate tokens server-side before processing - Delete used tokens immediately after verification ### Token Security - Generate tokens using crypto.randomBytes() - Store tokens with expiration timestamps - Use one-time tokens (delete after use) - Implement token rotation for resend requests ### Rate Limiting - Limit verification attempts per user - Implement exponential backoff - Monitor for suspicious activity - Block IPs with excessive attempts ## Testing & Validation ### Testing Checklist **Functional Testing** - Test link generation and expiration - Verify email delivery - Test app opening from email - Validate verification completion - Test expired link handling - Verify error handling **Security Testing** - Test with invalid tokens - Verify rate limiting works - Test token reuse prevention - Validate HTTPS enforcement - Test with expired tokens - Verify user isolation ### Sample Test Cases ```javascript // Jest test example describe('Email Verification', () => { test('should create verification link successfully', async () => { const mockUser = { id: '123', email: 'test@example.com' }; const mockToken = 'valid-token-123'; const link = await createVerificationLink(mockUser.id, mockToken); expect(link).toContain('redirectly.app'); expect(link).toContain(mockToken); }); test('should reject expired tokens', async () => { const expiredToken = 'expired-token'; const userId = '123'; const response = await request(app) .get(`/api/verify-email?token=${expiredToken}&user=${userId}`); expect(response.status).toBe(400); expect(response.body.error).toContain('expired'); }); test('should prevent token reuse', async () => { const token = 'used-token'; const userId = '123'; // First verification should succeed const firstResponse = await request(app) .get(`/api/verify-email?token=${token}&user=${userId}`); expect(firstResponse.status).toBe(200); // Second verification should fail const secondResponse = await request(app) .get(`/api/verify-email?token=${token}&user=${userId}`); expect(secondResponse.status).toBe(400); }); }); ``` ## You're Ready to Implement Email Verification! You now have everything you need to implement secure email verification with deep linking in your Flutter app. This approach provides a seamless user experience while maintaining high security standards. ## Related Articles & Resources - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Complete guide to implementing deferred deep linking in your Flutter apps. - [React Native Deferred Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) — Learn how to implement deferred deep linking in your React Native applications. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Secure Password Reset for Flutter Apps: A Complete Guide > Learn how to implement secure password reset flows in Flutter apps using deep links, with complete Flutter code examples and best practices. - Canonical: https://redirectly.app/blog/password-reset-flutter-apps - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-12 - Author: Dmytro Vyrych - Category: Security · 6 min read Implement secure password reset flows using temporary deep links that work perfectly with your Flutter app's deep linking system. Learn best practices for security, user experience, and implementation. ## Overview & Security Benefits **Why Use Deep Links for Password Reset?** Traditional password reset flows often redirect users to web pages, breaking the mobile app experience. With deep linking, users can reset their passwords directly within your Flutter app, maintaining context and providing a seamless experience. ### Security Benefits - Time-limited reset tokens (15-30 minutes) - One-time use tokens - Secure token generation - Rate limiting protection - HTTPS enforcement - Audit trail logging ### UX Benefits - Native app experience - No context switching - Maintained app state - Consistent UI/UX - Offline capability - Push notification integration ## Password Reset Flow ### Complete User Journey 1. **User Requests Reset** — User enters email in "Forgot Password" screen 2. **Token Generation** — Backend generates secure reset token and creates temporary link 3. **Email Delivery** — Reset link sent to user's email address 4. **Link Click & App Open** — User clicks link, app opens to password reset screen 5. **Password Update** — User enters new password, token is validated and consumed ## Backend Implementation ### 1. Password Reset Request Endpoint Create an endpoint that handles password reset requests: ```javascript app.post('/api/forgot-password', async (req, res) => { try { const { email } = req.body; // 1. Validate email exists const user = await findUserByEmail(email); if (!user) { // Don't reveal if email exists or not return res.json({ success: true, message: 'If the email exists, a reset link has been sent.' }); } // 2. Generate secure reset token const resetToken = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes // 3. Store reset token await storeResetToken(user.id, resetToken, expiresAt); // 4. Create temporary reset link const resetLink = await createPasswordResetLink(user.id, resetToken); // 5. Send reset email await sendPasswordResetEmail(email, resetLink); res.json({ success: true, message: 'If the email exists, a reset link has been sent.' }); } catch (error) { console.error('Password reset request error:', error); res.status(500).json({ error: 'Internal server error' }); } }); ``` ### 2. Reset Link Creation Create temporary reset links using Redirectly: ```javascript async function createPasswordResetLink(userId, token) { try { // Create temporary link that expires in 30 minutes const response = await redirectly.post('/v1/temp-links', { target: `https://redirectly.app/reset-password?token=${token}&user=${userId}`, ttlSeconds: 1800 // 30 minutes }); return response.data.url; } catch (error) { console.error('Failed to create reset link:', error); throw new Error('Failed to create reset link'); } } async function storeResetToken(userId, token, expiresAt) { // Store token in database with expiration await db.query( 'INSERT INTO password_reset_tokens (user_id, token, expires_at, used) VALUES (?, ?, ?, ?)', [userId, token, expiresAt, false] ); } ``` ### 3. Password Reset Handler Handle the actual password reset when the link is clicked: ```javascript app.post('/api/reset-password', async (req, res) => { try { const { token, userId, newPassword } = req.body; // 1. Validate token const tokenRecord = await db.query( 'SELECT * FROM password_reset_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW() AND used = false', [token, userId] ); if (!tokenRecord.length) { return res.status(400).json({ error: 'Invalid or expired reset token' }); } // 2. Validate new password strength if (!isValidPassword(newPassword)) { return res.status(400).json({ error: 'Password does not meet requirements' }); } // 3. Hash new password const hashedPassword = await hashPassword(newPassword); // 4. Update user password await db.query( 'UPDATE users SET password = ? WHERE id = ?', [hashedPassword, userId] ); // 5. Mark token as used await db.query( 'UPDATE password_reset_tokens SET used = true WHERE token = ?', [token] ); // 6. Invalidate all user sessions (optional) await invalidateUserSessions(userId); res.json({ success: true, message: 'Password reset successfully' }); } catch (error) { console.error('Password reset error:', error); res.status(500).json({ error: 'Password reset failed' }); } }); ``` ## Flutter Integration ### 1. Password Reset Service Create a service to handle password reset functionality: ```dart class PasswordResetService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static Future initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for incoming reset links _redirectly.onLinkClick.listen(_handlePasswordResetLink); // Check for initial link final initialLink = await _redirectly.getInitialLink(); if (initialLink != null) { _handlePasswordResetLink(initialLink); } } static void _handlePasswordResetLink(LinkClickEvent event) { if (event.error != null) { print('Link error: ${event.error}'); return; } final uri = Uri.parse(event.originalUrl); // Check if this is a password reset link if (uri.path.contains('/reset-password')) { final token = uri.queryParameters['token']; final userId = uri.queryParameters['user']; if (token != null && userId != null) { _navigateToPasswordReset(token, userId); } } } static void _navigateToPasswordReset(String token, String userId) { // Navigate to password reset screen with token Get.toNamed('/reset-password', arguments: { 'token': token, 'userId': userId, }); } static Future requestPasswordReset(String email) async { try { final response = await http.post( Uri.parse('${ApiConfig.baseUrl}/api/forgot-password'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': email}), ); return response.statusCode == 200; } catch (e) { print('Password reset request error: $e'); return false; } } static Future resetPassword(String token, String userId, String newPassword) async { try { final response = await http.post( Uri.parse('${ApiConfig.baseUrl}/api/reset-password'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'token': token, 'userId': userId, 'newPassword': newPassword, }), ); return response.statusCode == 200; } catch (e) { print('Password reset error: $e'); return false; } } } ``` ### 2. Password Reset UI Create the password reset screen: ```dart class PasswordResetScreen extends StatefulWidget { final String token; final String userId; const PasswordResetScreen({ Key? key, required this.token, required this.userId, }) : super(key: key); @override _PasswordResetScreenState createState() => _PasswordResetScreenState(); } class _PasswordResetScreenState extends State { final _formKey = GlobalKey(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); bool _isLoading = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Reset Password'), backgroundColor: Colors.transparent, elevation: 0, ), body: Padding( padding: EdgeInsets.all(24.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Enter your new password', style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), SizedBox(height: 32), TextFormField( controller: _passwordController, obscureText: true, decoration: InputDecoration( labelText: 'New Password', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a password'; } if (value.length < 8) { return 'Password must be at least 8 characters'; } return null; }, ), SizedBox(height: 16), TextFormField( controller: _confirmPasswordController, obscureText: true, decoration: InputDecoration( labelText: 'Confirm Password', border: OutlineInputBorder(), ), validator: (value) { if (value != _passwordController.text) { return 'Passwords do not match'; } return null; }, ), SizedBox(height: 32), ElevatedButton( onPressed: _isLoading ? null : _resetPassword, child: _isLoading ? CircularProgressIndicator(color: Colors.white) : Text('Reset Password'), ), ], ), ), ), ); } Future _resetPassword() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); final success = await PasswordResetService.resetPassword( widget.token, widget.userId, _passwordController.text, ); setState(() => _isLoading = false); if (success) { Get.snackbar( 'Success', 'Your password has been reset successfully!', backgroundColor: Colors.green, colorText: Colors.white, ); Get.offAllNamed('/login'); } else { Get.snackbar( 'Error', 'Failed to reset password. Please try again.', backgroundColor: Colors.red, colorText: Colors.white, ); } } } ``` ## Security Considerations **Critical Security Measures** - Use short expiration times (15-30 minutes) - Implement rate limiting (max 3 requests per hour per email) - Use cryptographically secure random tokens - Never reveal if email exists in system - Invalidate all user sessions after password reset - Log all password reset attempts for monitoring ### Rate Limiting - Limit reset requests per email/IP - Implement exponential backoff - Use Redis for distributed rate limiting - Monitor for abuse patterns ### Token Security - Generate 32+ byte random tokens - Store tokens with expiration - Mark tokens as used after reset - Clean up expired tokens regularly ## Testing & Validation ### Testing Checklist **Functional Testing** - Test reset request with valid email - Test reset request with invalid email - Verify email delivery - Test link expiration - Test password reset completion - Test token reuse prevention **Security Testing** - Test rate limiting - Test with expired tokens - Test with invalid tokens - Test session invalidation - Test password strength validation - Test HTTPS enforcement ## Secure Password Reset Implementation Complete! You now have a complete, secure password reset system that provides an excellent user experience while maintaining the highest security standards. Your users can reset their passwords seamlessly within your Flutter app. ## Related - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) — Complete guide to implementing deferred deep linking in your Flutter apps. - [Email Verification with Deep Linking](https://redirectly.app/blog/email-verification-deep-linking.md) — Learn how to implement secure email verification using temporary deep links. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Building a Referral Program with Deep Linking and Install Attribution > Create a powerful referral system that tracks installs, attributes users to referrers, and provides seamless onboarding experiences. - Canonical: https://redirectly.app/blog/referral-program-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-10 - Author: Dmytro Vyrych - Category: Growth · 10 min read Create a powerful referral system that tracks installs, attributes users to referrers, and provides seamless onboarding experiences. Learn how to build a complete referral program with deep linking and deferred deep linking. ## Overview & Benefits **Why Use Deep Linking for Referrals?** Traditional referral programs often lose track of users between click and install. With deep linking and deferred deep linking, you can track the complete user journey from referral click to app install to first action, ensuring accurate attribution and rewards. **Key Benefits:** - Complete user journey tracking - Accurate install attribution - Seamless onboarding experience - Real-time analytics and insights - Fraud prevention and validation - Automated reward distribution **Use Cases:** - User acquisition campaigns - Viral growth strategies - Influencer partnerships - Social media marketing - Event-based promotions - Cross-platform referrals ## System Architecture ### Referral Flow 1. **User Shares Referral Link** — Existing user shares personalized referral link via social media, messaging, etc. 2. **New User Clicks Link** — Prospect clicks referral link, gets redirected to app store or web page 3. **App Installation** — New user installs app, deferred deep linking system tracks the install 4. **Attribution & Rewards** — System attributes install to referrer and triggers reward distribution ## Backend Implementation ### 1. Generate Referral Link Create personalized referral links for users: ```javascript app.post('/api/generate-referral-link', async (req, res) => { try { const { userId } = req.body; // 1. Get user profile const user = await getUserById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } // 2. Create referral link with metadata const referralLink = await createReferralLink(userId, user.username); // 3. Store referral link in database await storeReferralLink(userId, referralLink); res.json({ success: true, referralLink: referralLink, shareText: `Join me on ${appName}! Use my referral link: ${referralLink}` }); } catch (error) { console.error('Referral link generation error:', error); res.status(500).json({ error: 'Failed to generate referral link' }); } }); async function createReferralLink(userId, username) { try { const response = await redirectly.post('/links', { slug: `ref-${username}-${Date.now()}`, target: `https://redirectly.app/onboarding?ref=${userId}`, metadata: { type: 'referral', referrerId: userId, referrerUsername: username, campaign: 'user_referral' } }); return response.data.url; } catch (error) { console.error('Failed to create referral link:', error); throw new Error('Failed to create referral link'); } } ``` ### 2. Handle App Install Attribution Track app installs and attribute them to referrers: ```javascript // Webhook endpoint for app install events app.post('/api/webhook/app-install', async (req, res) => { try { const { deviceId, installData } = req.body; // 1. Check if this install is attributed to a referral const attribution = await checkInstallAttribution(deviceId); if (attribution && attribution.referrerId) { // 2. Record successful referral await recordSuccessfulReferral(attribution); // 3. Trigger reward distribution await distributeReferralReward(attribution.referrerId, attribution.referredUserId); // 4. Send notifications await notifyReferralSuccess(attribution); } res.json({ success: true }); } catch (error) { console.error('App install attribution error:', error); res.status(500).json({ error: 'Attribution failed' }); } }); async function checkInstallAttribution(deviceId) { // Check if device was previously tracked from a referral link const attribution = await db.query( 'SELECT * FROM referral_attributions WHERE device_id = ? AND status = "pending"', [deviceId] ); return attribution.length > 0 ? attribution[0] : null; } async function recordSuccessfulReferral(attribution) { await db.query( 'UPDATE referral_attributions SET status = "completed", completed_at = NOW() WHERE id = ?', [attribution.id] ); // Update user referral stats await db.query( 'UPDATE users SET referral_count = referral_count + 1 WHERE id = ?', [attribution.referrer_id] ); } ``` ## Flutter Integration ### 1. Referral Service Create a service to handle referral functionality: ```dart class ReferralService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static Future initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for app install events _redirectly.onAppInstalled.listen(_handleAppInstall); // Listen for incoming referral links _redirectly.onLinkClick.listen(_handleReferralLink); } static void _handleAppInstall(AppInstallEvent event) { if (event.matched && event.username != null && event.slug != null) { // This is a successful referral install _processSuccessfulReferral(event); } else { // Organic install _processOrganicInstall(event); } } static void _handleReferralLink(LinkClickEvent event) { if (event.error != null) return; final uri = Uri.parse(event.originalUrl); final referrerId = uri.queryParameters['ref']; if (referrerId != null) { // Store referral attribution _storeReferralAttribution(referrerId); } } static Future _processSuccessfulReferral(AppInstallEvent event) async { try { // Notify backend of successful referral await http.post( Uri.parse('${ApiConfig.baseUrl}/api/referral-success'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'referrerUsername': event.username, 'referralSlug': event.slug, 'installData': event.toJson(), }), ); // Show referral success message _showReferralSuccessMessage(event.username!); } catch (e) { print('Referral processing error: $e'); } } static Future generateReferralLink() async { try { final response = await http.post( Uri.parse('${ApiConfig.baseUrl}/api/generate-referral-link'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'userId': AuthService.currentUserId}), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['referralLink']; } } catch (e) { print('Referral link generation error: $e'); } return null; } static void _showReferralSuccessMessage(String referrerUsername) { Get.snackbar( 'Welcome!', 'You were referred by $referrerUsername. Thanks for joining!', backgroundColor: Colors.green, colorText: Colors.white, duration: Duration(seconds: 5), ); } } ``` ### 2. Referral UI Components Create UI components for sharing referral links: ```dart class ReferralShareWidget extends StatefulWidget { @override _ReferralShareWidgetState createState() => _ReferralShareWidgetState(); } class _ReferralShareWidgetState extends State { String? _referralLink; bool _isLoading = false; @override void initState() { super.initState(); _loadReferralLink(); } Future _loadReferralLink() async { setState(() => _isLoading = true); _referralLink = await ReferralService.generateReferralLink(); setState(() => _isLoading = false); } @override Widget build(BuildContext context) { return Card( child: Padding( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Invite Friends', style: Theme.of(context).textTheme.headlineSmall, ), SizedBox(height: 8), Text( 'Share your referral link and earn rewards when friends join!', style: Theme.of(context).textTheme.bodyMedium, ), SizedBox(height: 16), if (_isLoading) Center(child: CircularProgressIndicator()) else if (_referralLink != null) Column( children: [ Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: Text( _referralLink!, style: TextStyle(fontFamily: 'monospace'), ), ), IconButton( icon: Icon(Icons.copy), onPressed: () { Clipboard.setData(ClipboardData(text: _referralLink!)); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Link copied to clipboard!')), ); }, ), ], ), ), SizedBox(height: 16), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => _shareReferralLink(), icon: Icon(Icons.share), label: Text('Share'), ), ), SizedBox(width: 8), Expanded( child: OutlinedButton.icon( onPressed: () => _showReferralStats(), icon: Icon(Icons.analytics), label: Text('Stats'), ), ), ], ), ], ), ], ), ), ); } void _shareReferralLink() { Share.share( 'Join me on ${AppConfig.appName}! Use my referral link: ${_referralLink}', subject: 'Join ${AppConfig.appName}!', ); } void _showReferralStats() { Get.toNamed('/referral-stats'); } } ``` ## Analytics & Tracking ### Key Metrics to Track **Referral Metrics:** - Total referral links generated - Click-through rates by channel - Conversion rates (click to install) - Time to install after click - Referral completion rates - Revenue per referral **User Behavior:** - Referrer engagement levels - Referred user retention - Viral coefficient (K-factor) - Network effects analysis - Channel performance comparison - Geographic distribution ### Analytics Dashboard Create analytics endpoints to track referral performance: ```javascript app.get('/api/referral-analytics', async (req, res) => { try { const { userId, timeRange = '30d' } = req.query; const analytics = await getReferralAnalytics(userId, timeRange); res.json({ success: true, analytics: { totalReferrals: analytics.totalReferrals, successfulReferrals: analytics.successfulReferrals, conversionRate: analytics.conversionRate, totalRewards: analytics.totalRewards, topChannels: analytics.topChannels, dailyStats: analytics.dailyStats, viralCoefficient: analytics.viralCoefficient } }); } catch (error) { console.error('Analytics error:', error); res.status(500).json({ error: 'Failed to fetch analytics' }); } }); async function getReferralAnalytics(userId, timeRange) { const startDate = getStartDate(timeRange); const [referrals, successful, rewards, channels] = await Promise.all([ db.query('SELECT COUNT(*) as count FROM referral_attributions WHERE referrer_id = ? AND created_at >= ?', [userId, startDate]), db.query('SELECT COUNT(*) as count FROM referral_attributions WHERE referrer_id = ? AND status = "completed" AND created_at >= ?', [userId, startDate]), db.query('SELECT SUM(amount) as total FROM referral_rewards WHERE referrer_id = ? AND created_at >= ?', [userId, startDate]), db.query('SELECT channel, COUNT(*) as count FROM referral_attributions WHERE referrer_id = ? AND created_at >= ? GROUP BY channel ORDER BY count DESC', [userId, startDate]) ]); return { totalReferrals: referrals[0].count, successfulReferrals: successful[0].count, conversionRate: referrals[0].count > 0 ? (successful[0].count / referrals[0].count) * 100 : 0, totalRewards: rewards[0].total || 0, topChannels: channels, dailyStats: await getDailyReferralStats(userId, startDate), viralCoefficient: await calculateViralCoefficient(userId, startDate) }; } ``` ## Best Practices **Growth Optimization:** - Offer meaningful rewards for referrals - Make sharing easy with pre-written messages - Track and optimize conversion funnels - A/B test different reward structures - Implement gamification elements - Use social proof and testimonials **Fraud Prevention:** - Implement device fingerprinting - Set reasonable reward limits - Monitor for suspicious patterns - Require email verification - Implement cooldown periods - Use machine learning for detection **Pro Tips for Success:** - Start with a simple referral program and iterate based on data - Focus on quality over quantity - engaged users refer better users - Use deep linking to create seamless onboarding experiences - Track the complete user journey from click to first value - Regularly analyze and optimize your referral funnel - Consider seasonal campaigns and special events for referral boosts ## Your Referral Program is Ready You now have a complete referral program with deep linking, install attribution, and comprehensive analytics. This system will help you grow your user base organically while providing excellent user experiences. Get started at https://dashboard.redirectly.app/ or view the [documentation](https://docs.redirectly.app). ## Related - [Deferred Deep Linking Guide](https://redirectly.app/deferred-deep-linking.md) — Master deferred deep linking and install attribution for your mobile apps. - [Marketing Campaigns with Deep Linking](https://redirectly.app/blog/marketing-campaigns-deep-linking.md) — Maximize your marketing ROI with deep linking campaigns and detailed attribution tracking. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Marketing Campaigns with Deep Linking: From Click to Conversion > Supercharge your marketing campaigns with deep linking attribution. Learn campaign tracking, conversion optimization, and ROI measurement for apps. - Canonical: https://redirectly.app/blog/marketing-campaigns-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-08 - Author: Dmytro Vyrych - Category: Marketing · 7 min read Maximize your marketing ROI with deep linking campaigns that provide seamless user experiences and detailed attribution tracking. Learn how to create, track, and optimize marketing campaigns that drive real results. ## Table of Contents 1. Overview & Benefits 2. Campaign Types 3. Implementation 4. Tracking & Analytics 5. Optimization Strategies 6. Best Practices ## Overview & Benefits **Why Use Deep Linking for Marketing Campaigns?** Traditional marketing campaigns often lose users in the transition from ad click to app install. Deep linking bridges this gap, providing seamless experiences that maintain context and drive higher conversion rates. With proper attribution tracking, you can measure the true ROI of your marketing efforts. ### Key Benefits - Complete attribution tracking - Seamless user experiences - Higher conversion rates - Real-time campaign analytics - A/B testing capabilities - Cross-platform consistency ### Campaign Types - Social media advertising - Email marketing campaigns - Influencer partnerships - Content marketing - Retargeting campaigns - Event-based promotions ## Campaign Types ### 1. Social Media Campaigns Create targeted campaigns for Facebook, Instagram, Twitter, TikTok, and other social platforms: **Facebook/Instagram Ads** - Use UTM parameters for tracking - Create custom landing experiences - A/B test different creative assets - Track conversion events **TikTok/YouTube Shorts** - Leverage trending hashtags - Create engaging video content - Use influencer partnerships - Track viral coefficient ### 2. Email Marketing Campaigns Design email campaigns that drive app installs and engagement: **Newsletter Campaigns** - Personalized deep links - Segment-based targeting - Automated drip campaigns - Performance tracking **Transactional Emails** - Welcome series - Abandoned cart recovery - Feature announcements - Re-engagement campaigns ### 3. Influencer Partnerships Collaborate with influencers to reach new audiences: **Micro-Influencers** - Higher engagement rates - Authentic content creation - Cost-effective partnerships - Niche audience targeting **Macro-Influencers** - Large reach potential - Brand awareness building - Professional content quality - Cross-platform presence ## Implementation ### 1. Campaign Link Generation Create campaign-specific links with detailed tracking parameters: ```javascript app.post('/api/create-campaign-link', async (req, res) => { try { const { campaignName, targetUrl, source, medium, content, term, metadata = {} } = req.body; // 1. Generate unique campaign slug const campaignSlug = generateCampaignSlug(campaignName); // 2. Create campaign link with UTM parameters const campaignLink = await createCampaignLink({ slug: campaignSlug, target: targetUrl, metadata: { campaign: campaignName, source: source, medium: medium, content: content, term: term, ...metadata } }); // 3. Store campaign in database await storeCampaign({ name: campaignName, slug: campaignSlug, link: campaignLink, source: source, medium: medium, status: 'active', created_at: new Date() }); res.json({ success: true, campaignLink: campaignLink, campaignId: campaignSlug, trackingUrl: `${campaignLink}?utm_source=${source}&utm_medium=${medium}&utm_campaign=${campaignName}` }); } catch (error) { console.error('Campaign creation error:', error); res.status(500).json({ error: 'Failed to create campaign' }); } }); async function createCampaignLink({ slug, target, metadata }) { try { const response = await redirectly.post('/links', { slug: slug, target: target, metadata: metadata }); return response.data.url; } catch (error) { console.error('Failed to create campaign link:', error); throw new Error('Failed to create campaign link'); } } function generateCampaignSlug(campaignName) { const timestamp = Date.now(); const cleanName = campaignName.toLowerCase() .replace(/[^a-z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); return `${cleanName}-${timestamp}`; } ``` ### 2. Flutter Campaign Tracking Track campaign performance in your Flutter app: ```dart class CampaignTrackingService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static Future initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for campaign link clicks _redirectly.onLinkClick.listen(_handleCampaignClick); // Listen for app installs from campaigns _redirectly.onAppInstalled.listen(_handleCampaignInstall); } static void _handleCampaignClick(LinkClickEvent event) { if (event.error != null) return; final uri = Uri.parse(event.originalUrl); final campaignData = _extractCampaignData(uri); if (campaignData != null) { // Store campaign attribution _storeCampaignAttribution(campaignData); // Track campaign click _trackCampaignEvent('click', campaignData); } } static void _handleCampaignInstall(AppInstallEvent event) { if (event.matched && event.link != null) { final campaignData = _extractCampaignDataFromLink(event.link!); if (campaignData != null) { // Track successful campaign conversion _trackCampaignEvent('install', campaignData); // Show campaign-specific onboarding _showCampaignOnboarding(campaignData); } } } static Map? _extractCampaignData(Uri uri) { final utmSource = uri.queryParameters['utm_source']; final utmMedium = uri.queryParameters['utm_medium']; final utmCampaign = uri.queryParameters['utm_campaign']; final utmContent = uri.queryParameters['utm_content']; final utmTerm = uri.queryParameters['utm_term']; if (utmSource != null && utmMedium != null && utmCampaign != null) { return { 'source': utmSource, 'medium': utmMedium, 'campaign': utmCampaign, 'content': utmContent ?? '', 'term': utmTerm ?? '', 'timestamp': DateTime.now().toIso8601String(), }; } return null; } static Future _trackCampaignEvent(String eventType, Map campaignData) async { try { await http.post( Uri.parse('${ApiConfig.baseUrl}/api/track-campaign-event'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'eventType': eventType, 'campaignData': campaignData, 'deviceInfo': await _getDeviceInfo(), }), ); } catch (e) { print('Campaign tracking error: $e'); } } static void _showCampaignOnboarding(Map campaignData) { // Show campaign-specific welcome message final campaignName = campaignData['campaign'] ?? 'Unknown Campaign'; Get.snackbar( 'Welcome!', 'Thanks for joining from our $campaignName campaign!', backgroundColor: Colors.blue, colorText: Colors.white, duration: Duration(seconds: 5), ); // Navigate to campaign-specific onboarding flow Get.toNamed('/onboarding', arguments: { 'campaign': campaignData, 'source': 'campaign' }); } } ``` ## Tracking & Analytics ### Key Metrics to Track **Campaign Performance** - Click-through rates (CTR) - Conversion rates (click to install) - Cost per acquisition (CPA) - Return on ad spend (ROAS) - Lifetime value (LTV) by campaign - Attribution window analysis **User Behavior** - Time to install after click - First session engagement - Retention rates by source - Feature adoption rates - In-app purchase conversion - Churn rates by campaign ### Analytics Dashboard Create comprehensive campaign analytics: ```javascript app.get('/api/campaign-analytics', async (req, res) => { try { const { campaignId, timeRange = '30d', source, medium } = req.query; const analytics = await getCampaignAnalytics({ campaignId, timeRange, source, medium }); res.json({ success: true, analytics: { overview: analytics.overview, performance: analytics.performance, sources: analytics.sources, devices: analytics.devices, geographic: analytics.geographic, trends: analytics.trends } }); } catch (error) { console.error('Campaign analytics error:', error); res.status(500).json({ error: 'Failed to fetch analytics' }); } }); async function getCampaignAnalytics({ campaignId, timeRange, source, medium }) { const startDate = getStartDate(timeRange); const filters = buildFilters({ campaignId, source, medium, startDate }); const [ clicks, installs, conversions, revenue, sources, devices, geographic ] = await Promise.all([ getCampaignClicks(filters), getCampaignInstalls(filters), getCampaignConversions(filters), getCampaignRevenue(filters), getSourceBreakdown(filters), getDeviceBreakdown(filters), getGeographicBreakdown(filters) ]); return { overview: { totalClicks: clicks.total, totalInstalls: installs.total, conversionRate: clicks.total > 0 ? (installs.total / clicks.total) * 100 : 0, totalRevenue: revenue.total, roas: revenue.total / (clicks.total * 0.5), // Assuming $0.50 per click cpa: installs.total > 0 ? (clicks.total * 0.5) / installs.total : 0 }, performance: { dailyClicks: clicks.daily, dailyInstalls: installs.daily, dailyRevenue: revenue.daily }, sources: sources, devices: devices, geographic: geographic, trends: { clickTrend: calculateTrend(clicks.daily), installTrend: calculateTrend(installs.daily), revenueTrend: calculateTrend(revenue.daily) } }; } ``` ## Optimization Strategies ### A/B Testing - Test different landing pages - Experiment with UTM parameters - Try various creative assets - Test different call-to-actions - Optimize for different audiences - Measure statistical significance ### Performance Optimization - Optimize for high-converting sources - Adjust bids based on LTV - Retarget engaged users - Scale successful campaigns - Pause underperforming ads - Optimize for mobile experience **Mobile-First Optimization** - Ensure fast loading times on mobile devices - Optimize for thumb-friendly interactions - Test on various screen sizes and devices - Use mobile-specific ad formats and placements - Implement progressive web app features for better engagement - Track mobile-specific conversion funnels ## Best Practices ### Campaign Setup - Use consistent UTM parameter naming - Create clear campaign hierarchies - Set up proper conversion tracking - Implement proper attribution windows - Use meaningful campaign names - Document campaign objectives ### Data Management - Regular data quality audits - Implement data retention policies - Use proper data privacy controls - Set up automated reporting - Monitor for data anomalies - Backup campaign data regularly **Pro Tips for Success** - Start with a small budget and scale successful campaigns - Focus on quality over quantity - better to have fewer, high-converting campaigns - Use deep linking to create personalized experiences for different campaign sources - Regularly analyze and optimize your campaign performance - Test different creative assets and messaging to find what resonates - Leverage seasonal trends and events for campaign timing ## Your Marketing Campaigns are Ready! You now have a complete marketing campaign system with deep linking, comprehensive tracking, and optimization tools. This setup will help you maximize your marketing ROI and drive meaningful user acquisition. Documentation: https://docs.redirectly.app ## Related - [Deferred Deep Linking Guide](https://redirectly.app/deferred-deep-linking.md) — Master deferred deep linking and install attribution for your mobile apps. - [Redirectly Home](https://redirectly.app/index.md) — Explore our smart deferred deep linking platform and services. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # App Onboarding with Deferred Deep Linking: Complete User Journey > Create seamless app onboarding using deferred deep linking. Learn user flow optimization, first-time experience design, and retention strategies. - Canonical: https://redirectly.app/blog/app-onboarding-deferred-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native - Published: 2024-01-05 - Author: Dmytro Vyrych - Category: UX · 9 min read Design perfect onboarding experiences that work even when users install your app after clicking a marketing link. Learn how to create seamless user journeys from click to first value with deferred deep linking. ## Table of Contents 1. Overview & Benefits 2. User Journey Design 3. Implementation 4. Onboarding Flows 5. Best Practices ## Overview & Benefits **What is Deferred Deep Linking for Onboarding?** Deferred deep linking allows users to click a marketing link, install your app, and then land exactly where they intended - even if they didn't have the app installed when they first clicked. This creates seamless onboarding experiences that maintain context and drive higher engagement rates. ### Key Benefits - Seamless user experience - Higher conversion rates - Context preservation - Reduced friction - Better user retention - Improved engagement ### Use Cases - Marketing campaign onboarding - Referral program flows - Feature-specific onboarding - Content deep linking - Social media campaigns - Email marketing flows ## User Journey Design ### Complete User Journey 1. **User Clicks Marketing Link** — User clicks a link from email, social media, or advertisement 2. **App Store Redirect** — User is redirected to app store to install the app 3. **App Installation** — User installs and opens the app for the first time 4. **Deferred Deep Link Activation** — App detects the original intent and navigates to the right screen 5. **Contextual Onboarding** — User receives personalized onboarding based on their original intent ## Implementation ### 1. Onboarding Service Setup Create a service to handle deferred deep linking and onboarding: ```dart class OnboardingService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static bool _hasProcessedInitialLink = false; static Future initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for app install events _redirectly.onAppInstalled.listen(_handleAppInstall); // Check for initial link on app start await _checkInitialLink(); } static Future _checkInitialLink() async { if (_hasProcessedInitialLink) return; final initialLink = await _redirectly.getInitialLink(); if (initialLink != null) { await _processOnboardingLink(initialLink); _hasProcessedInitialLink = true; } } static void _handleAppInstall(AppInstallEvent event) { if (event.matched && event.link != null) { _processOnboardingLink(event.link!); } } static Future _processOnboardingLink(LinkClickEvent event) async { if (event.error != null) { print('Onboarding link error: ${event.error}'); return; } final uri = Uri.parse(event.originalUrl); final onboardingData = _extractOnboardingData(uri); if (onboardingData != null) { // Store onboarding context await _storeOnboardingContext(onboardingData); // Navigate to appropriate onboarding flow _navigateToOnboardingFlow(onboardingData); } } static Map? _extractOnboardingData(Uri uri) { // Extract onboarding parameters from the link final source = uri.queryParameters['source']; final campaign = uri.queryParameters['campaign']; final feature = uri.queryParameters['feature']; final referrer = uri.queryParameters['ref']; if (source != null) { return { 'source': source, 'campaign': campaign, 'feature': feature, 'referrer': referrer, 'timestamp': DateTime.now().toIso8601String(), }; } return null; } static Future _storeOnboardingContext(Map data) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('onboarding_context', jsonEncode(data)); } static void _navigateToOnboardingFlow(Map data) { final source = data['source'] as String; switch (source) { case 'marketing': _navigateToMarketingOnboarding(data); break; case 'referral': _navigateToReferralOnboarding(data); break; case 'feature': _navigateToFeatureOnboarding(data); break; default: _navigateToDefaultOnboarding(); } } static void _navigateToMarketingOnboarding(Map data) { Get.offAllNamed('/onboarding/marketing', arguments: { 'campaign': data['campaign'], 'source': data['source'], }); } static void _navigateToReferralOnboarding(Map data) { Get.offAllNamed('/onboarding/referral', arguments: { 'referrer': data['referrer'], 'source': data['source'], }); } static void _navigateToFeatureOnboarding(Map data) { Get.offAllNamed('/onboarding/feature', arguments: { 'feature': data['feature'], 'source': data['source'], }); } static void _navigateToDefaultOnboarding() { Get.offAllNamed('/onboarding/default'); } } ``` ### 2. Initialize in main.dart Initialize the onboarding service when your app starts: ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize onboarding service await OnboardingService.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'My App', theme: ThemeData( primarySwatch: Colors.blue, ), home: AuthService.isLoggedIn() ? HomeScreen() : OnboardingWrapper(), routes: { '/onboarding/default': (context) => DefaultOnboardingScreen(), '/onboarding/marketing': (context) => MarketingOnboardingScreen(), '/onboarding/referral': (context) => ReferralOnboardingScreen(), '/onboarding/feature': (context) => FeatureOnboardingScreen(), '/home': (context) => HomeScreen(), }, ); } } class OnboardingWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder?>( future: _getOnboardingContext(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return SplashScreen(); } final context = snapshot.data; if (context != null) { // Navigate to appropriate onboarding based on context WidgetsBinding.instance.addPostFrameCallback((_) { OnboardingService._navigateToOnboardingFlow(context); }); } return DefaultOnboardingScreen(); }, ); } Future?> _getOnboardingContext() async { final prefs = await SharedPreferences.getInstance(); final contextString = prefs.getString('onboarding_context'); if (contextString != null) { return jsonDecode(contextString) as Map; } return null; } } ``` ## Onboarding Flows ### Marketing Campaign Onboarding Create personalized onboarding experiences for users coming from marketing campaigns: ```dart class MarketingOnboardingScreen extends StatefulWidget { final String? campaign; final String? source; const MarketingOnboardingScreen({ Key? key, this.campaign, this.source, }) : super(key: key); @override _MarketingOnboardingScreenState createState() => _MarketingOnboardingScreenState(); } class _MarketingOnboardingScreenState extends State { int _currentStep = 0; final PageController _pageController = PageController(); final List _steps = [ OnboardingStep( title: 'Welcome to Our App!', description: 'Thanks for joining us from our marketing campaign.', image: 'assets/onboarding/welcome.png', ), OnboardingStep( title: 'Discover Features', description: 'Explore the amazing features we have to offer.', image: 'assets/onboarding/features.png', ), OnboardingStep( title: 'Get Started', description: 'You're all set! Start exploring the app.', image: 'assets/onboarding/complete.png', ), ]; @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ // Progress indicator LinearProgressIndicator( value: (_currentStep + 1) / _steps.length, backgroundColor: Colors.grey[300], valueColor: AlwaysStoppedAnimation(Colors.blue), ), // Onboarding content Expanded( child: PageView.builder( controller: _pageController, onPageChanged: (index) { setState(() => _currentStep = index); }, itemCount: _steps.length, itemBuilder: (context, index) { final step = _steps[index]; return OnboardingPage( step: step, campaign: widget.campaign, source: widget.source, ); }, ), ), // Navigation buttons Padding( padding: EdgeInsets.all(24.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (_currentStep > 0) TextButton( onPressed: _previousStep, child: Text('Back'), ) else SizedBox.shrink(), ElevatedButton( onPressed: _nextStep, child: Text(_currentStep == _steps.length - 1 ? 'Get Started' : 'Next'), ), ], ), ), ], ), ), ); } void _previousStep() { if (_currentStep > 0) { _pageController.previousPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } void _nextStep() { if (_currentStep < _steps.length - 1) { _pageController.nextPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); } else { _completeOnboarding(); } } void _completeOnboarding() { // Track onboarding completion AnalyticsService.trackEvent('onboarding_completed', { 'source': widget.source, 'campaign': widget.campaign, 'flow': 'marketing', }); // Navigate to main app Get.offAllNamed('/home'); } } ``` ## Best Practices ### User Experience - Keep onboarding flows short and focused - Use clear, actionable language - Provide visual feedback and progress indicators - Allow users to skip non-essential steps - Test onboarding flows on different devices - Gather user feedback and iterate ### Technical Implementation - Handle edge cases gracefully - Implement proper error handling - Use analytics to track completion rates - Store onboarding state persistently - Optimize for performance - Test deferred deep linking thoroughly **Pro Tips for Success** - Personalize onboarding based on the source of the user - Use deferred deep linking to maintain context across the user journey - A/B test different onboarding flows to optimize conversion rates - Track key metrics like completion rates and time to first value - Provide clear value propositions early in the onboarding process - Use progressive disclosure to avoid overwhelming new users ## 🎉 Perfect Onboarding Experiences Await! You now have a complete onboarding system with deferred deep linking that provides seamless user experiences. This setup will help you convert more users and improve retention rates significantly. ## Related Articles & Resources - [Deferred Deep Linking Guide](https://redirectly.app/deferred-deep-linking.md) — Master deferred deep linking and install attribution for your mobile apps. - [Marketing Campaigns with Deep Linking](https://redirectly.app/blog/marketing-campaigns-deep-linking.md) — Maximize your marketing ROI with deep linking campaigns and detailed attribution tracking. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Linking Glossary > Definitions of deep linking, attribution, and mobile growth terms: Universal Links, App Links, AASA, deferred deep linking, and more. - Canonical: https://redirectly.app/glossary - [Android App Links](https://redirectly.app/glossary/android-app-links.md): Android's verified deep linking mechanism that uses Digital Asset Links to associate HTTPS URLs with an app, allowing links to open directly in the app without a disambiguation dialog. - [App Indexing](https://redirectly.app/glossary/app-indexing.md): The process of making in-app content discoverable and surfaceable in search engine results, allowing users to open app screens directly from Google or Apple search. - [App Tracking Transparency (ATT)](https://redirectly.app/glossary/app-tracking-transparency.md): Apple's iOS framework that requires apps to request user permission before tracking their activity across other companies' apps and websites. - [Apple App Site Association (AASA)](https://redirectly.app/glossary/aasa.md): A JSON configuration file hosted on a web domain that tells iOS which URL paths should open in a native app, enabling Universal Links. - [Contextual Deep Linking](https://redirectly.app/glossary/contextual-deep-linking.md): Deep linking that carries metadata (context) about the user's pre-click journey, enabling personalized in-app experiences based on the source, campaign, or content that brought them. - [Deep Linking](https://redirectly.app/glossary/deep-linking.md): A technique that uses URIs to link directly to a specific screen or piece of content within a mobile app, rather than simply launching the app's home screen. - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md): A deep linking technique that preserves the intended destination through the app install process, routing users to specific content after they install and open the app for the first time. - [Mobile Attribution](https://redirectly.app/glossary/attribution.md): The process of identifying which marketing channel, campaign, or touchpoint led a user to install or engage with a mobile app. - [Mobile Measurement Partner (MMP)](https://redirectly.app/glossary/mobile-measurement-partner.md): A third-party analytics company that helps app marketers measure and attribute installs and in-app events across advertising channels. - [QR Code Deep Linking](https://redirectly.app/glossary/qr-code-deep-linking.md): The practice of encoding deep links within QR codes so that scanning routes users directly to specific in-app content, bridging offline and online experiences. - [SKAdNetwork](https://redirectly.app/glossary/skadnetwork.md): Apple's privacy-preserving framework for measuring the effectiveness of advertising campaigns that drive app installs, without revealing user-level data. - [Smart App Banner](https://redirectly.app/glossary/smart-banner.md): A banner displayed on a mobile website that promotes the native app, using deep links to route users to the corresponding in-app content when tapped. - [Universal Links](https://redirectly.app/glossary/universal-links.md): An iOS feature that allows HTTPS URLs to open directly in a native app instead of Safari, verified through an Apple App Site Association (AASA) file. - [URI Schemes](https://redirectly.app/glossary/uri-schemes.md): Custom URL protocols (like myapp://path) that enable basic deep linking into mobile apps, though without the security and fallback benefits of Universal Links or App Links. - [UTM Parameters](https://redirectly.app/glossary/utm-parameters.md): Standardized URL query parameters (utm_source, utm_medium, utm_campaign, etc.) used to track the effectiveness of marketing campaigns by identifying traffic sources. --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deep Linking > A technique that uses URIs to link directly to a specific screen or piece of content within a mobile app, rather than simply launching the app's home screen. - Canonical: https://redirectly.app/glossary/deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition Deep linking is a technique that uses Uniform Resource Identifiers (URIs) to direct users to a specific location within a mobile application, rather than simply opening the app to its default screen. Just as a web URL can point to a specific page on a website, a deep link points to a specific screen, product, or piece of content inside an app. For example, instead of linking to an e-commerce app's home screen and expecting users to search for a product, a deep link can take them directly to that product's detail page. This creates a seamless, web-like navigation experience for mobile apps. ## How it works When a user taps a deep link, the operating system checks if the target app is installed. If the app is installed, the OS launches it and passes the URI so the app can route to the correct screen. The three main types of deep links are: **URI Schemes** — Custom protocols like `myapp://product/123`. These are the simplest form but lack fallback behavior and can conflict between apps. **Universal Links (iOS)** — Standard HTTPS URLs that Apple verifies against an Apple App Site Association (AASA) file hosted on your domain. If the app is installed, iOS opens it directly; otherwise, the URL opens in Safari. **Android App Links** — Similar to Universal Links but for Android. They use Digital Asset Links (assetlinks.json) for domain verification and open the app directly when installed. ## Why it matters Deep linking is fundamental to mobile growth because it removes friction from the user journey. Apps that implement deep linking typically see higher conversion rates, better retention, and improved user engagement. Key benefits include: - **Higher conversion rates** — Users land exactly where they need to, reducing drop-off - **Better attribution** — Track which campaigns, channels, and creatives drive installs and engagement - **Seamless cross-platform experiences** — Bridge the gap between web, email, social media, and your app - **Improved re-engagement** — Bring users back to specific content via push notifications, email, or ads ## FAQ ### What is the difference between deep linking and regular linking? Regular links open a website in a browser, while deep links open a specific screen inside a mobile app. Deep links provide a more seamless experience by taking users directly to the relevant content within the app. ### Do deep links work if the app is not installed? Standard deep links (URI schemes) do not work if the app is not installed — the link simply fails. However, deferred deep linking solves this by redirecting users to the app store first, then routing them to the intended content after installation. ### How do I implement deep linking in my mobile app? You need to configure your app to handle specific URL patterns (URI schemes, Universal Links, or App Links), set up server-side verification files (AASA for iOS, assetlinks.json for Android), and add routing logic in your app. Platforms like Redirectly simplify this with SDKs for Flutter and React Native. ## Related terms - [Universal Links](https://redirectly.app/glossary/universal-links.md) - [Android App Links](https://redirectly.app/glossary/android-app-links.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) - [URI Schemes](https://redirectly.app/glossary/uri-schemes.md) ## Related pages - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [What is Deep Linking? (Blog)](https://redirectly.app/blog/what-is-deep-linking.md) - [Deep Link Generator Tool](https://redirectly.app/tools/deep-link-generator.md) - [Deep Link Tester](https://redirectly.app/tools/deep-link-tester.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Universal Links > An iOS feature that allows HTTPS URLs to open directly in a native app instead of Safari, verified through an Apple App Site Association (AASA) file. - Canonical: https://redirectly.app/glossary/universal-links - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition Universal Links are Apple's standard mechanism for deep linking on iOS. They allow standard HTTPS URLs to seamlessly open content in a native iOS app instead of Safari. Unlike custom URI schemes, Universal Links use your existing web domain, which means the same URL works whether the user has your app installed or not. When a user taps a Universal Link and has your app installed, iOS opens the app directly and passes the URL for in-app routing. If the app is not installed, the URL opens normally in Safari, where you can show a web version or redirect to the App Store. ## How it works Universal Links rely on a two-part verification system: **1. Apple App Site Association (AASA) file** — A JSON file hosted at `https://yourdomain.com/.well-known/apple-app-site-association` that declares which URL paths your app can handle. Apple's CDN fetches and caches this file when your app is installed. **2. App entitlements** — Your iOS app must include an Associated Domains entitlement listing the domains it can open (e.g., `applinks:yourdomain.com`). When both are configured correctly, iOS verifies the association at install time. After verification, tapping any matching URL opens your app instead of Safari. This happens at the OS level — no JavaScript redirect or interstitial page is needed. ## Why it matters Universal Links are the recommended deep linking method on iOS because they provide: - **No interstitial dialogs** — The app opens instantly without "Open in app?" prompts - **Graceful fallback** — If the app isn't installed, the URL works as a normal web link - **Security** — Domain ownership is verified by Apple, preventing link hijacking - **No conflicts** — Unlike URI schemes, Universal Links can't be claimed by other apps - **User trust** — Uses standard HTTPS URLs that users recognize and trust ## FAQ ### How do I test if my Universal Links are working? You can use Redirectly's free AASA Validator tool to check your Apple App Site Association file for errors. For on-device testing, long-press a Universal Link in Notes or Messages — if you see "Open in [App Name]", the link is working correctly. ### Why are my Universal Links opening in Safari instead of my app? Common causes include: an invalid or unreachable AASA file, mismatched bundle IDs or team IDs, the Associated Domains entitlement not being configured, or the user having previously chosen to open links in Safari (which can be reset by long-pressing the link). ### Do Universal Links work with all iOS versions? Universal Links were introduced in iOS 9. All modern iOS versions support them. However, the AASA file format was updated in iOS 14 to support more flexible pattern matching with the "details" array format. ## Related terms - [Apple App Site Association (AASA)](https://redirectly.app/glossary/aasa.md) - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Android App Links](https://redirectly.app/glossary/android-app-links.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) ## Related pages - [AASA Validator](https://redirectly.app/aasa-validator.md) - [Universal Links Not Working (Debug)](https://redirectly.app/debug/universal-links-not-working.md) - [Flutter Universal Links Setup](https://redirectly.app/blog/flutter-universal-links-ios-setup.md) - [React Native Universal Links](https://redirectly.app/blog/react-native-universal-links-ios.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Deferred Deep Linking > A deep linking technique that preserves the intended destination through the app install process, routing users to specific content after they install and open the app for the first time. - Canonical: https://redirectly.app/glossary/deferred-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition Deferred deep linking extends standard deep linking to work even when the target app is not yet installed. The "deferred" part means the deep link destination is preserved (deferred) through the app store installation process, so users are routed to the correct in-app content after they install and open the app for the first time. For example, a user clicks a link to a specific product in your app. Since they don't have the app installed, they're redirected to the App Store or Play Store. After installing and opening the app, deferred deep linking ensures they land on that exact product page — not the generic onboarding screen. ## How it works Deferred deep linking works by matching a pre-install click to a post-install app open. Here's the typical flow: **1. Click capture** — When the user clicks a link, the deep linking platform records device fingerprint data (IP address, device model, OS version, screen size, locale) and the intended destination. **2. Redirect to store** — Since the app isn't installed, the user is redirected to the appropriate app store. **3. Install & open** — The user installs the app and opens it for the first time. **4. Match & route** — The app's SDK contacts the deep linking platform, which matches the device fingerprint to the original click. The preserved link data is returned, and the app routes to the intended content. Modern platforms like Redirectly use probabilistic fingerprinting combined with deterministic signals to achieve high match accuracy without requiring user-identifiable data. ## Why it matters Deferred deep linking is critical for mobile growth because most marketing touchpoints reach users who don't yet have your app: - **Higher new-user conversion** — Users who land on relevant content after install are far more likely to engage than those who see a generic home screen - **Install attribution** — Know exactly which campaign, ad, or referral drove each install - **Seamless referral programs** — The referrer's context (invite code, shared content) passes through the install - **Better onboarding** — Personalize the first-time experience based on what brought the user to your app ## FAQ ### How is deferred deep linking different from regular deep linking? Regular deep linking only works when the app is already installed — it routes users to specific in-app content. Deferred deep linking adds the ability to preserve that destination through the app install process, so new users still land on the right content after installing. ### Does deferred deep linking work on both iOS and Android? Yes. Deferred deep linking works on both platforms, though the underlying matching techniques differ. On iOS, privacy restrictions (App Tracking Transparency, Private Relay) require fingerprinting and probabilistic matching. On Android, Google Play Install Referrer provides a deterministic matching signal. ### How accurate is deferred deep linking? Modern deferred deep linking platforms achieve 90-95%+ match rates by combining multiple signals: device fingerprinting, IP matching, Google Play Install Referrer (Android), and Apple's attribution APIs. Redirectly uses a multi-signal approach to maximize accuracy across both platforms. ## Related terms - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) - [Universal Links](https://redirectly.app/glossary/universal-links.md) - [Android App Links](https://redirectly.app/glossary/android-app-links.md) ## Related pages - [Deferred Deep Linking Guide](https://redirectly.app/deferred-deep-linking.md) - [Flutter Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md) - [React Native Deferred Deep Linking](https://redirectly.app/react-native-deferred-deep-linking.md) - [Deferred Deep Link Not Firing (Debug)](https://redirectly.app/debug/deferred-deep-link-not-firing.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Android App Links > Android's verified deep linking mechanism that uses Digital Asset Links to associate HTTPS URLs with an app, allowing links to open directly in the app without a disambiguation dialog. - Canonical: https://redirectly.app/glossary/android-app-links - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition Android App Links are Android's equivalent of iOS Universal Links. They allow verified HTTPS URLs to open directly in your Android app without showing a disambiguation dialog (the "Open with..." chooser). Google introduced App Links in Android 6.0 (API 23) as an improvement over basic intent filters. Unlike basic deep links that use custom URI schemes or unverified intent filters, Android App Links require domain verification through a Digital Asset Links file (assetlinks.json). This verification proves that your app and website are owned by the same entity, preventing other apps from intercepting your links. ## How it works Android App Links use a verification system similar to iOS Universal Links: **1. assetlinks.json** — A JSON file hosted at `https://yourdomain.com/.well-known/assetlinks.json` that declares the relationship between your domain and your app's package name and signing certificate. **2. Intent filter with autoVerify** — Your app's AndroidManifest.xml includes intent filters with `android:autoVerify="true"`, which tells Android to verify the domain association at install time. **3. Verification flow** — When the app is installed, Android fetches the assetlinks.json file from each declared domain and checks that the app's package name and signing certificate match. If verification succeeds, the app is set as the default handler for those URLs. **4. Link handling** — After verification, tapping a matching URL opens the app directly. The app receives an Intent containing the URL and can extract path parameters to route to the correct screen. ## Why it matters Android App Links provide a significantly better user experience than basic deep links: - **No disambiguation dialog** — Links open in your app instantly, without asking the user to choose between apps - **Security** — Domain verification prevents other apps from claiming your URLs - **Instant Apps support** — App Links can trigger Android Instant App experiences - **Google search integration** — Verified App Links can surface in Google search results - **Consistency with web** — Uses the same HTTPS URLs as your website ## FAQ ### What is the difference between Android App Links and deep links? Android deep links is a broad term that includes URI schemes, intent-based links, and App Links. Android App Links specifically refers to verified HTTPS links that use Digital Asset Links (assetlinks.json) for domain verification. App Links open directly in the app without a chooser dialog, while unverified deep links may show a disambiguation prompt. ### How do I verify my assetlinks.json file is correct? Use Redirectly's free Asset Links Validator tool to check your assetlinks.json file for formatting errors, missing fields, and certificate mismatches. You can also use Google's Digital Asset Links API for programmatic verification. ### Do Android App Links work on all Android versions? Android App Links with auto-verification require Android 6.0 (API 23) or higher. On older versions, intent filters still work but will show a disambiguation dialog. As of 2026, over 95% of active Android devices support App Links. ## Related terms - [Universal Links](https://redirectly.app/glossary/universal-links.md) - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [URI Schemes](https://redirectly.app/glossary/uri-schemes.md) - [Apple App Site Association (AASA)](https://redirectly.app/glossary/aasa.md) ## Related pages - [Asset Links Validator](https://redirectly.app/assetlinks-validator.md) - [Android App Links Not Opening (Debug)](https://redirectly.app/debug/android-app-links-not-opening.md) - [Flutter Android App Links Setup](https://redirectly.app/blog/flutter-android-app-links-setup.md) - [assetlinks.json Guide](https://redirectly.app/blog/assetlinks-json-guide.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Apple App Site Association (AASA) > A JSON configuration file hosted on a web domain that tells iOS which URL paths should open in a native app, enabling Universal Links. - Canonical: https://redirectly.app/glossary/aasa - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition The Apple App Site Association (AASA) file is a JSON configuration file that establishes a verified link between a web domain and one or more iOS apps. It is the cornerstone of Universal Links on iOS — without a valid AASA file, Universal Links cannot function. The AASA file is hosted at a well-known path on your domain: `https://yourdomain.com/.well-known/apple-app-site-association`. It must be served over HTTPS with the correct content type and without any redirects. Apple's CDN fetches this file when a user installs your app to verify the domain-app association. ## How it works The AASA file contains a JSON object that specifies which apps are associated with your domain and which URL paths each app can handle: **File location** — Must be at `/.well-known/apple-app-site-association` (no file extension). **Content type** — Must be served as `application/json`. **App association** — Each entry includes your team ID and bundle ID in the format `TEAMID.com.example.app`, plus an array of URL path patterns the app handles. **Pattern matching** — Supports wildcards (`*`) and exclusion patterns (`NOT`). For example, `/products/*` matches any product URL, while `NOT /products/admin/*` excludes admin paths. **Apple CDN caching** — Apple fetches the AASA file through its own CDN (app-site-association.cdn-apple.com) and caches it. Changes may take 24-48 hours to propagate. In iOS 14+, Apple introduced an alternate CDN endpoint that updates more frequently. ## Why it matters The AASA file is a critical piece of your mobile deep linking infrastructure: - **Required for Universal Links** — No AASA file means no Universal Links functionality on iOS - **Security verification** — Proves domain ownership to Apple, preventing unauthorized apps from intercepting your URLs - **Flexible path matching** — Control exactly which URLs open your app and which stay in Safari - **Multiple app support** — A single domain can associate with multiple apps (e.g., main app and lite version) - **Shared Web Credentials** — AASA also enables AutoFill of passwords between your website and app ## FAQ ### How do I create an AASA file? Create a JSON file with the applinks key containing your app's team ID and bundle ID, plus the URL paths to handle. Host it at /.well-known/apple-app-site-association on your domain over HTTPS. Use Redirectly's AASA Validator to verify your file is correct before deploying. ### Why are my Universal Links not working after updating the AASA file? Apple caches AASA files through its CDN, so changes can take 24-48 hours to propagate. Other common issues include: serving the file with redirects (not allowed), incorrect content type, invalid JSON syntax, or mismatched team/bundle IDs. Use an AASA validator to diagnose the specific issue. ### Does the AASA file need a .json extension? No. The AASA file must NOT have a file extension. It should be served at the exact path /.well-known/apple-app-site-association with the application/json content type. Adding a .json extension will cause Universal Links to fail. ## Related terms - [Universal Links](https://redirectly.app/glossary/universal-links.md) - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Android App Links](https://redirectly.app/glossary/android-app-links.md) - [URI Schemes](https://redirectly.app/glossary/uri-schemes.md) ## Related pages - [AASA Validator Tool](https://redirectly.app/aasa-validator.md) - [AASA File Errors (Debug)](https://redirectly.app/debug/aasa-file-errors.md) - [AASA File Format Guide](https://redirectly.app/blog/aasa-file-format-ios-guide.md) - [Universal Links Not Working](https://redirectly.app/debug/universal-links-not-working.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # URI Schemes > Custom URL protocols (like myapp://path) that enable basic deep linking into mobile apps, though without the security and fallback benefits of Universal Links or App Links. - Canonical: https://redirectly.app/glossary/uri-schemes - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition URI schemes (also called custom URL schemes or custom protocols) are the original deep linking mechanism for mobile apps. They use a custom protocol prefix — like `myapp://` or `twitter://` — instead of the standard `https://` to route users to specific content within an app. For example, `myapp://product/123` tells the operating system to open "myapp" and navigate to product 123. URI schemes have been available since the earliest versions of iOS and Android, making them the most widely supported — but also the most limited — form of deep linking. ## How it works URI schemes work by registering a custom protocol handler with the operating system: **On iOS** — You declare your custom scheme in the app's Info.plist under `CFBundleURLSchemes`. When any URL with that scheme is triggered, iOS launches your app and passes the full URL to your app delegate. **On Android** — You add an intent filter in AndroidManifest.xml that matches your custom scheme. Android routes matching URLs to your activity, where you extract the path and parameters. **No verification** — Unlike Universal Links and App Links, URI schemes require no server-side verification file. Any app can register any scheme, which is both a benefit (simple setup) and a drawback (no security guarantees). ## Why it matters While URI schemes are being replaced by Universal Links and App Links for most use cases, they still serve important purposes: - **Simplest to implement** — No server-side configuration needed - **Broadest compatibility** — Works on all iOS and Android versions - **App-to-app communication** — Useful for launching other apps with specific parameters However, URI schemes have significant limitations: - **No fallback** — If the app isn't installed, the link fails silently or shows an error - **No uniqueness** — Multiple apps can register the same scheme, causing conflicts - **No security** — No domain verification means any app could intercept your links - **Blocked in some contexts** — Many email clients and social apps strip custom URI schemes ## FAQ ### Should I use URI schemes or Universal Links? For user-facing deep links, always prefer Universal Links (iOS) and App Links (Android). They provide security, graceful fallback to web, and no disambiguation dialogs. Reserve URI schemes for app-to-app communication or backwards compatibility with very old OS versions. ### Can two apps register the same URI scheme? Yes, and this is a major limitation of URI schemes. If multiple apps register the same scheme, the behavior is undefined — the OS may open any of them. This is why Universal Links and App Links, which use domain verification, are more secure. ## Related terms - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Universal Links](https://redirectly.app/glossary/universal-links.md) - [Android App Links](https://redirectly.app/glossary/android-app-links.md) - [Apple App Site Association (AASA)](https://redirectly.app/glossary/aasa.md) ## Related pages - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [Deep Link vs Universal Links vs App Links](https://redirectly.app/blog/deep-linking-vs-universal-links-vs-app-links.md) - [Deep Link Generator](https://redirectly.app/tools/deep-link-generator.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # App Indexing > The process of making in-app content discoverable and surfaceable in search engine results, allowing users to open app screens directly from Google or Apple search. - Canonical: https://redirectly.app/glossary/app-indexing - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition App indexing is the process of making content within your mobile app discoverable through search engines like Google and Apple Search. When properly configured, your in-app content can appear in search results alongside traditional web pages, with deep links that open the content directly in your app. For example, if a user searches for "red running shoes" on Google and your e-commerce app has that product, Google can show an app result that opens directly to that product page — bypassing the web entirely. ## How it works App indexing works by connecting your app content to web URLs that search engines can crawl: **Google App Indexing** — Google indexes app content through several mechanisms: - **Web-app association** — If your website already ranks in Google and you've set up Android App Links or Universal Links, Google automatically associates web results with app deep links - **Firebase App Indexing API** — Allows your app to report viewable content to Google's index, even for content that doesn't have a web equivalent - **Googlebot crawling** — Google can render and index the content of your Instant Apps **Apple Spotlight** — iOS apps can use Core Spotlight and NSUserActivity APIs to index content that appears in iOS Spotlight search and Siri suggestions. **Structured data** — Adding app link metadata to your website's structured data helps search engines understand the app-web content relationship. ## Why it matters App indexing is a powerful organic growth channel: - **Free app installs** — Users discover your app through searches they're already making - **Higher engagement** — Users who enter through specific content are more likely to engage than those from generic app store listings - **Competitive advantage** — Most apps don't implement indexing, so those that do gain prominent search placement - **Re-engagement** — Existing users can rediscover content in your app through search - **SEO synergy** — Your web SEO efforts directly benefit your app visibility ## FAQ ### How do I get my app indexed by Google? The easiest path is to set up Android App Links with your existing website. If your website already ranks in Google, app links will be automatically associated with search results. For content without web equivalents, use the Firebase App Indexing API to submit content directly. ### Does app indexing help with app store rankings? App indexing primarily affects web search results, not app store search. However, increased installs and engagement from web search can indirectly improve your app store rankings through higher install velocity and better engagement metrics. ## Related terms - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Universal Links](https://redirectly.app/glossary/universal-links.md) - [Android App Links](https://redirectly.app/glossary/android-app-links.md) - [Smart App Banner](https://redirectly.app/glossary/smart-banner.md) ## Related pages - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [AASA Validator](https://redirectly.app/aasa-validator.md) - [Asset Links Validator](https://redirectly.app/assetlinks-validator.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Smart App Banner > A banner displayed on a mobile website that promotes the native app, using deep links to route users to the corresponding in-app content when tapped. - Canonical: https://redirectly.app/glossary/smart-banner - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition A smart app banner is a promotional bar displayed on a mobile website that encourages visitors to open or install the corresponding native app. The "smart" part means the banner is contextually aware — it knows whether the app is installed and can pass the current page's content context to the app via a deep link. Apple provides a native smart banner for iOS Safari via a simple meta tag. For cross-browser and cross-platform support, most apps implement custom smart banners that offer more control over design, targeting, and deep linking behavior. ## How it works **Apple's native Smart Banner** — Add a meta tag to your HTML: ``. Safari displays a standardized banner at the top of the page. If the app is installed, tapping "Open" passes the app-argument as a deep link. If not installed, it links to the App Store. **Custom smart banners** — Most apps build custom banners for more flexibility: - Detect the user's platform (iOS/Android) and show the appropriate store link - Include deep link parameters that map to the current web page content - Use deferred deep linking to preserve context through the install process - Control design, dismissal behavior, frequency capping, and A/B testing - Track banner impressions, clicks, and resulting installs for attribution ## Why it matters Smart banners are one of the highest-ROI mobile growth tactics because they target users who are already engaged with your content: - **Convert web traffic to app users** — Mobile web visitors are high-intent users who already know your brand - **Contextual routing** — Users land on the same content in-app, not a generic home screen - **Non-intrusive** — Unlike interstitials, smart banners don't block content and comply with Google's guidelines - **Measurable** — Track the full funnel from banner impression to app install to in-app action - **Cross-platform** — Custom banners work on both iOS and Android, across all browsers ## FAQ ### What is the difference between Apple's smart banner and a custom smart banner? Apple's native smart banner is a simple meta tag that only works in Safari on iOS. Custom smart banners are built with HTML/CSS/JS and work across all browsers and platforms. Custom banners offer more control over design, targeting, deep linking, and analytics, but require more development effort. ### Do smart banners affect SEO? Smart banners implemented as standard HTML do not negatively affect SEO. However, full-screen app interstitials that block content can be penalized by Google. Stick to banner-style promotions that don't obstruct the page content. ## Related terms - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) - [App Indexing](https://redirectly.app/glossary/app-indexing.md) - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) ## Related pages - [Deep Linking Guide](https://redirectly.app/deep-linking-guide.md) - [QR Codes Use Case](https://redirectly.app/use-cases/qr-codes.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # SKAdNetwork > Apple's privacy-preserving framework for measuring the effectiveness of advertising campaigns that drive app installs, without revealing user-level data. - Canonical: https://redirectly.app/glossary/skadnetwork - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition SKAdNetwork (SKAN) is Apple's privacy-centric framework for measuring the effectiveness of ad campaigns that drive app installations on iOS. It provides aggregated, limited attribution data to advertisers without exposing individual user identifiers or allowing cross-app tracking. Introduced alongside App Tracking Transparency (ATT) in iOS 14, SKAdNetwork has become the primary way to measure paid advertising performance on iOS. Version 4.0 (iOS 16.1+) introduced hierarchical conversion values, multiple postbacks, and web-to-app attribution support. ## How it works SKAdNetwork operates as a three-party system managed by Apple: **1. Ad network registration** — Ad networks register with Apple and receive a network ID. They sign ad impressions with their credentials. **2. Ad display & click** — When a user sees or taps an ad, the ad network calls SKAdNetwork APIs to register the impression. Apple cryptographically signs this interaction. **3. App install & conversion** — After the user installs and opens the advertised app, the app can set a conversion value (a number representing the quality of the install) within a limited time window. **4. Postback delivery** — After a timer expires (24-72+ hours), Apple sends a postback to the ad network containing the campaign ID, conversion value, and attribution data — but no user identifiers. Postbacks are delayed and may include noise (randomized data) for privacy. **SKAN 4.0 improvements** — Multiple conversion windows (0-2, 3-7, 8-35 days), fine and coarse conversion values based on crowd anonymity, and up to 3 postbacks per install. ## Why it matters SKAdNetwork has fundamentally changed mobile marketing measurement: - **Required for iOS ad attribution** — With ATT opt-in rates around 25-30%, SKAN is the primary attribution method for most iOS users - **Privacy compliant** — No user-level tracking, no IDFA required, no cross-app data sharing - **Limitations to understand** — Delayed reporting (24-72+ hours), limited campaign granularity (100 campaigns in SKAN 4.0), no real-time optimization, conversion value constraints - **Impacts deep linking** — SKAN attribution is separate from deep linking; combining both requires careful implementation to provide a good user experience while maintaining measurement accuracy ## FAQ ### How is SKAdNetwork different from traditional mobile attribution? Traditional attribution (like IDFA-based tracking) provided real-time, user-level data about which ad drove each install. SKAdNetwork provides aggregated, delayed data without user identifiers. It's more privacy-preserving but offers less granularity and slower feedback loops for campaign optimization. ### Do I still need SKAdNetwork if users opt into ATT tracking? Yes. SKAdNetwork runs for all users regardless of ATT consent. For users who opt into tracking, you can use IDFA-based attribution alongside SKAN for richer data. But since most users opt out of tracking, SKAN is essential for measuring the majority of your iOS ad spend. ## Related terms - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) - [App Tracking Transparency (ATT)](https://redirectly.app/glossary/app-tracking-transparency.md) - [Mobile Measurement Partner (MMP)](https://redirectly.app/glossary/mobile-measurement-partner.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) ## Related pages - [Mobile Install Attribution Guide](https://redirectly.app/blog/mobile-install-attribution-guide.md) - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) - [Adjust Alternative](https://redirectly.app/adjust-alternative.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Mobile Attribution > The process of identifying which marketing channel, campaign, or touchpoint led a user to install or engage with a mobile app. - Canonical: https://redirectly.app/glossary/attribution - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition Mobile attribution is the science of determining which marketing touchpoint — an ad click, an email link, a social media post, a referral, or any other interaction — led a user to install or engage with a mobile app. It answers the fundamental marketing question: "What caused this user to take this action?" Attribution connects the dots between marketing spend and actual results: installs, sign-ups, purchases, and other in-app events. Without attribution, marketers are spending blindly — unable to measure ROI or optimize campaigns. ## How it works Mobile attribution uses several matching techniques to connect a pre-install interaction to a post-install app open: **Deterministic matching** — The most accurate method, using unique identifiers: - **IDFA/GAID** — Device advertising IDs (requires ATT consent on iOS) - **Google Play Install Referrer** — Passes referrer data through the Play Store install process - **Deep link click IDs** — Unique tokens embedded in deep links **Probabilistic matching** — When deterministic IDs aren't available: - **Device fingerprinting** — Matching based on IP address, device model, OS version, screen resolution, and locale - **Statistical modeling** — Using machine learning to find the most likely match **Attribution models** determine how credit is assigned: - **Last-click** — The last touchpoint before install gets full credit (most common) - **Multi-touch** — Credit is distributed across multiple touchpoints - **View-through** — Gives partial credit to ad impressions even without a click ## Why it matters Attribution is the foundation of data-driven mobile marketing: - **Measure ROI** — Know exactly how much each install costs and how much revenue each channel generates - **Optimize spend** — Shift budget from underperforming channels to those driving real results - **Detect fraud** — Identify suspicious install patterns that indicate ad fraud - **Personalize onboarding** — Tailor the first-time experience based on the acquisition source - **Prove marketing impact** — Quantify the business value of marketing campaigns to stakeholders ## FAQ ### What is the difference between attribution and analytics? Attribution answers "where did this user come from?" — connecting installs and events to specific marketing touchpoints. Analytics answers "what is the user doing?" — tracking in-app behavior, engagement, and user flows. Both are essential: attribution measures acquisition effectiveness, analytics measures product effectiveness. ### How does iOS privacy (ATT) affect mobile attribution? App Tracking Transparency (ATT) requires user consent before accessing the IDFA for attribution. Since most users opt out, iOS attribution increasingly relies on SKAdNetwork for paid campaigns and probabilistic matching (fingerprinting) for organic and owned channels. Platforms like Redirectly use privacy-compliant deferred deep linking for attribution without requiring IDFA. ## Related terms - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) - [SKAdNetwork](https://redirectly.app/glossary/skadnetwork.md) - [App Tracking Transparency (ATT)](https://redirectly.app/glossary/app-tracking-transparency.md) - [Mobile Measurement Partner (MMP)](https://redirectly.app/glossary/mobile-measurement-partner.md) - [UTM Parameters](https://redirectly.app/glossary/utm-parameters.md) ## Related pages - [Mobile Install Attribution Guide](https://redirectly.app/blog/mobile-install-attribution-guide.md) - [UTM Parameters for Mobile Apps](https://redirectly.app/blog/utm-parameters-mobile-apps.md) - [UTM Builder Tool](https://redirectly.app/tools/utm-builder.md) - [Branch vs AppsFlyer vs Adjust](https://redirectly.app/blog/branch-vs-appsflyer-vs-adjust.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Mobile Measurement Partner (MMP) > A third-party analytics company that helps app marketers measure and attribute installs and in-app events across advertising channels. - Canonical: https://redirectly.app/glossary/mobile-measurement-partner - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition A Mobile Measurement Partner (MMP) is a third-party company that provides unbiased measurement and attribution for mobile app marketing campaigns. MMPs act as a neutral intermediary between app developers and advertising networks, determining which ad network, campaign, or creative drove each app install and subsequent in-app event. The term "partner" reflects the trusted third-party role: because MMPs are independent from both the advertiser and the ad network, their attribution data is considered unbiased — critical for accurate billing and performance measurement. ## How it works MMPs integrate with both sides of the advertising ecosystem: **App-side SDK** — The MMP SDK is installed in your app. It captures install events, in-app events (purchases, sign-ups, etc.), and device data for attribution matching. **Ad network integrations** — MMPs maintain pre-built integrations with hundreds of ad networks (Meta, Google, TikTok, etc.) to receive click and impression data. **Attribution logic** — When your app is opened for the first time, the MMP SDK sends device data to the MMP servers, which match it against recent ad clicks/impressions from integrated networks. The MMP determines which network gets credit based on the attribution model (usually last-click). **Reporting** — MMPs provide dashboards showing install counts, cost data, ROI metrics, and funnel analysis broken down by network, campaign, creative, and more. Major MMPs include AppsFlyer, Adjust, Kochava, Branch, and Singular. ## Why it matters MMPs solve a critical trust problem in mobile advertising: - **Unbiased measurement** — Ad networks have an incentive to over-claim installs; MMPs provide neutral, trusted attribution - **Cross-network deduplication** — If a user clicks ads on multiple networks before installing, the MMP ensures only one network gets credit - **Fraud detection** — MMPs identify fake installs, click flooding, click injection, and other fraud tactics - **Consolidated reporting** — One dashboard for all ad networks instead of checking each network individually - **Cost aggregation** — Combine spend data from all channels to calculate true cost-per-install and ROAS ## FAQ ### Do I need an MMP if I use Redirectly? Redirectly and MMPs serve different primary purposes. Redirectly focuses on deferred deep linking and organic/owned-channel attribution — ensuring users land on the right content after install. MMPs focus on paid advertising attribution across ad networks. Many apps use both: Redirectly for deep linking and user routing, and an MMP for paid media measurement. ### What is the difference between an MMP and a deep linking platform? An MMP primarily measures which advertising campaigns drive installs and in-app events. A deep linking platform (like Redirectly) primarily routes users to specific in-app content via links. There's overlap — some MMPs offer deep linking and some deep linking platforms offer attribution — but the core focus differs. ## Related terms - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) - [SKAdNetwork](https://redirectly.app/glossary/skadnetwork.md) - [App Tracking Transparency (ATT)](https://redirectly.app/glossary/app-tracking-transparency.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) ## Related pages - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) - [Adjust Alternative](https://redirectly.app/adjust-alternative.md) - [Kochava Alternative](https://redirectly.app/kochava-alternative.md) - [Branch vs AppsFlyer vs Adjust](https://redirectly.app/blog/branch-vs-appsflyer-vs-adjust.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # App Tracking Transparency (ATT) > Apple's iOS framework that requires apps to request user permission before tracking their activity across other companies' apps and websites. - Canonical: https://redirectly.app/glossary/app-tracking-transparency - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition App Tracking Transparency (ATT) is Apple's privacy framework, introduced in iOS 14.5, that requires apps to explicitly ask users for permission before tracking their activity across other companies' apps and websites. This primarily affects access to the IDFA (Identifier for Advertisers), the device-level identifier used for ad targeting and attribution. When an app wants to track a user, it must present a system-level permission prompt. If the user chooses "Ask App Not to Track," the app cannot access the IDFA and must not engage in cross-app tracking. Industry-wide opt-in rates are approximately 25-30%, meaning the majority of iOS users are not trackable. ## How it works ATT operates at the OS level with a simple but impactful permission flow: **1. Permission request** — The app calls the ATT framework to request tracking authorization. iOS displays a standardized prompt: "[App Name] would like permission to track you across apps and websites owned by other companies." **2. User decision** — The user sees two options: "Ask App Not to Track" or "Allow." The app can add a single line of custom text explaining why tracking helps (e.g., "This helps us show you relevant ads"). **3. IDFA access** — If the user allows tracking, the app can access the IDFA for attribution and ad targeting. If denied, the IDFA returns all zeros (00000000-0000-0000-0000-000000000000). **4. System-wide setting** — Users can also disable tracking globally in Settings > Privacy > Tracking, which blocks all apps from even showing the prompt. **Impact on attribution** — Without IDFA, traditional deterministic attribution is impossible. The industry has shifted to SKAdNetwork for paid campaigns and probabilistic/fingerprint-based methods for organic attribution. ## Why it matters ATT has been the most significant change to the mobile advertising ecosystem in a decade: - **Reduced ad targeting accuracy** — Without IDFA, personalized ad targeting is less precise on iOS - **Attribution gaps** — Marketers can't track individual user journeys across channels as easily - **SKAdNetwork adoption** — SKAN became the primary paid attribution method on iOS - **Privacy-first solutions** — Drove innovation in contextual targeting, on-device processing, and privacy-preserving measurement - **Deep linking importance** — Platforms like Redirectly that use deep link-based attribution (not IDFA) became more valuable as a privacy-compliant alternative ## FAQ ### How does ATT affect deep linking? ATT does not directly affect deep linking. Universal Links, App Links, and deferred deep linking work independently of IDFA and ATT consent. However, ATT affects the attribution layer — knowing which campaign drove an install. Deep linking platforms like Redirectly use link-based attribution methods that don't require IDFA, making them ATT-compliant by design. ### Should I still show the ATT prompt? It depends on your monetization model. If you rely heavily on personalized advertising (especially with Meta or Google), showing the prompt and getting even 25% opt-in improves ad performance and attribution. If your app doesn't rely on ad-driven revenue, you may skip the prompt entirely — but you then cannot access IDFA at all. ## Related terms - [SKAdNetwork](https://redirectly.app/glossary/skadnetwork.md) - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) - [Mobile Measurement Partner (MMP)](https://redirectly.app/glossary/mobile-measurement-partner.md) ## Related pages - [Mobile Install Attribution Guide](https://redirectly.app/blog/mobile-install-attribution-guide.md) - [AppsFlyer Alternative](https://redirectly.app/apps-flyer-alternative.md) - [Adjust Alternative](https://redirectly.app/adjust-alternative.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # Contextual Deep Linking > Deep linking that carries metadata (context) about the user's pre-click journey, enabling personalized in-app experiences based on the source, campaign, or content that brought them. - Canonical: https://redirectly.app/glossary/contextual-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition Contextual deep linking combines standard deep linking with metadata that describes the context of the user's journey. Beyond simply routing a user to a specific screen, contextual deep links carry information about where the user came from, what campaign they saw, who referred them, and any other custom data the developer attaches to the link. For example, a contextual deep link for a referral program might carry the referrer's user ID, the specific product being shared, and the campaign name. When the recipient opens the app — even after a fresh install — the app can use this context to credit the referrer, show the shared product, and display a personalized welcome message. ## How it works Contextual deep linking works by attaching key-value parameters to deep links and preserving them through the entire user journey: **1. Link creation** — A deep link is created with custom parameters: `https://yourapp.link/invite?referrer=user123&product=456&campaign=summer_promo` **2. Click capture** — When the user clicks, the deep linking platform records all parameters along with the device fingerprint and timestamp. **3. App open** — Whether the app was already installed (standard deep link) or needed to be installed first (deferred deep link), the app's SDK retrieves the full context from the platform. **4. Personalized experience** — The app uses the retrieved context to: - Route to the specific content (product 456) - Apply the referral credit (from user123) - Track the campaign source (summer_promo) - Customize the UI ("Welcome! user123 invited you") The key differentiator from basic deep linking is that contextual deep links carry arbitrary metadata that survives the install process, enabling personalization that goes far beyond simple routing. ## Why it matters Contextual deep linking is the foundation of modern mobile growth strategies: - **Personalized onboarding** — First-time users see relevant content instead of a generic welcome screen, dramatically improving activation rates - **Referral program tracking** — Automatically credit referrers and reward both parties without manual code entry - **Campaign attribution** — Know exactly which campaign, channel, and creative drove each install and downstream event - **Content sharing** — When users share content from your app, recipients get deep-linked to that exact content - **Cross-platform continuity** — Preserve user context when transitioning from web to app, email to app, or social to app ## FAQ ### How is contextual deep linking different from regular deep linking? Regular deep linking routes users to a specific screen in an app. Contextual deep linking does the same routing but also carries custom metadata (referrer ID, campaign name, promo codes, etc.) that the app can use to personalize the experience. Think of it as deep linking + data passthrough. ### Does contextual deep linking work for new users who don't have the app? Yes, when combined with deferred deep linking. The context (all custom parameters) is preserved through the app store install process and delivered to the app on first open. This is what makes it powerful for referral programs and personalized marketing campaigns targeting new users. ## Related terms - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) - [UTM Parameters](https://redirectly.app/glossary/utm-parameters.md) ## Related pages - [Referral Programs Use Case](https://redirectly.app/use-cases/referral-programs.md) - [App Onboarding Use Case](https://redirectly.app/use-cases/app-onboarding.md) - [Paid Ads Use Case](https://redirectly.app/use-cases/paid-ads.md) - [Deep Link Generator](https://redirectly.app/tools/deep-link-generator.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # UTM Parameters > Standardized URL query parameters (utm_source, utm_medium, utm_campaign, etc.) used to track the effectiveness of marketing campaigns by identifying traffic sources. - Canonical: https://redirectly.app/glossary/utm-parameters - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition UTM (Urchin Tracking Module) parameters are standardized tags appended to URLs to track the source, medium, and campaign of web and app traffic. Originally developed by Urchin Software (later acquired by Google to create Google Analytics), UTM parameters are the industry standard for campaign-level traffic attribution. A typical UTM-tagged URL looks like: `https://yoursite.com/page?utm_source=facebook&utm_medium=paid_social&utm_campaign=summer_sale&utm_content=blue_banner&utm_term=running_shoes` ## How it works UTM parameters are key-value pairs added to the query string of any URL. The five standard parameters are: **utm_source** — Identifies which site or platform sent the traffic (e.g., `google`, `facebook`, `newsletter`) **utm_medium** — Identifies the marketing medium (e.g., `cpc`, `email`, `social`, `organic`) **utm_campaign** — Identifies the specific campaign name (e.g., `summer_sale_2026`, `product_launch`) **utm_content** (optional) — Differentiates similar content or links within the same campaign (e.g., `hero_banner` vs `sidebar_link`) **utm_term** (optional) — Identifies paid search keywords (e.g., `running+shoes`) When a user clicks a UTM-tagged link, analytics tools (Google Analytics, Mixpanel, Amplitude, etc.) capture these parameters and associate them with the user's session, allowing you to attribute conversions back to specific campaigns. **UTM for mobile apps** — When combined with deep linking, UTM parameters can pass through the install process via deferred deep linking, enabling campaign attribution for app installs — not just web visits. ## Why it matters UTM parameters are essential for any data-driven marketing strategy: - **Campaign measurement** — Know exactly which campaigns drive traffic, installs, and revenue - **Channel comparison** — Compare performance across email, social, paid ads, and other channels - **Budget optimization** — Allocate spend to the highest-performing campaigns and channels - **A/B testing** — Use utm_content to compare different creatives, copy, or landing pages - **Universal standard** — Supported by virtually every analytics tool, making data portable and consistent ## Best practices - Use lowercase consistently (utm_source=Facebook vs utm_source=facebook creates duplicate entries) - Create a UTM naming convention document and share it with your team - Never use UTM parameters on internal links — they overwrite the original traffic source - Keep campaign names descriptive but concise (e.g., summer_sale_2026 not ss26) - Use a UTM builder tool to ensure consistency and avoid typos ## FAQ ### Do UTM parameters work with mobile deep links? Yes. UTM parameters can be added to deep links just like any URL. When used with a deferred deep linking platform like Redirectly, UTM parameters are preserved through the app install process, allowing you to attribute app installs to specific campaigns. Use Redirectly's UTM Builder tool to generate properly tagged deep links. ### How many UTM parameters should I use? At minimum, always use utm_source, utm_medium, and utm_campaign — these three provide the core campaign tracking data. Add utm_content when you have multiple links in the same campaign (e.g., different ad creatives), and utm_term for paid search keyword tracking. ## Related terms - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Contextual Deep Linking](https://redirectly.app/glossary/contextual-deep-linking.md) ## Related pages - [UTM Builder Tool](https://redirectly.app/tools/utm-builder.md) - [UTM Parameters for Mobile Apps (Blog)](https://redirectly.app/blog/utm-parameters-mobile-apps.md) - [Mobile Install Attribution Guide](https://redirectly.app/blog/mobile-install-attribution-guide.md) - [Paid Ads Use Case](https://redirectly.app/use-cases/paid-ads.md) --- Full site index for AI agents: https://redirectly.app/llms.txt --- # QR Code Deep Linking > The practice of encoding deep links within QR codes so that scanning routes users directly to specific in-app content, bridging offline and online experiences. - Canonical: https://redirectly.app/glossary/qr-code-deep-linking - Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native ## Definition QR code deep linking is the practice of encoding a deep link URL within a QR code, allowing users to scan a physical or digital code and be routed directly to specific content within a mobile app. It bridges the gap between offline experiences (print ads, product packaging, in-store displays, event materials) and mobile app engagement. Unlike basic QR codes that open a website, deep-linked QR codes can open the app directly to a specific screen — a product page, a promotion, a menu, an event check-in — while preserving attribution data about where the scan occurred. ## How it works QR code deep linking combines standard QR code technology with deep linking infrastructure: **1. Generate a deep link** — Create a deep link URL that points to the intended in-app content, including any UTM parameters or custom metadata for attribution. **2. Encode as QR code** — Generate a QR code image containing the deep link URL. The URL should be a Universal Link or App Link for the best experience (app opens directly, with web fallback). **3. User scans** — The user scans the QR code with their phone camera. Modern iOS and Android devices have built-in QR scanners. **4. Routing** — If the app is installed, the deep link opens the app directly to the target content. If not installed, the user can be redirected to the app store with deferred deep linking to preserve the destination. **Dynamic vs Static QR codes** — Static QR codes contain a fixed URL and can't be changed after printing. Dynamic QR codes point to a redirect URL that can be updated, enabling A/B testing, analytics, and destination changes without reprinting. ## Why it matters QR code deep linking unlocks offline-to-app attribution — a channel that's otherwise nearly impossible to measure: - **Bridge offline to digital** — Connect physical touchpoints (packaging, print, TV, billboards) to your app - **Trackable offline campaigns** — Measure which physical locations, materials, or events drive the most app engagement - **Frictionless experience** — One scan takes users directly to relevant content, no typing URLs or searching - **Versatile use cases** — Restaurant menus, product packaging, event check-in, loyalty programs, in-store promotions - **Post-COVID adoption** — QR code usage has grown dramatically, with most users comfortable scanning codes regularly ## FAQ ### How do I create a QR code with a deep link? First, create a deep link URL using a platform like Redirectly (or use the free Deep Link Generator tool). Then encode that URL into a QR code using any QR code generator. For best results, use a Universal Link or App Link URL so the app opens directly on scan. ### Can QR codes trigger deferred deep linking? Yes. If the QR code contains a deep link from a deferred deep linking platform like Redirectly, users who don't have the app installed will be redirected to the app store. After installing, they'll be routed to the intended content — the same deferred deep linking flow that works with any link. ## Related terms - [Deep Linking](https://redirectly.app/glossary/deep-linking.md) - [Deferred Deep Linking](https://redirectly.app/glossary/deferred-deep-linking.md) - [UTM Parameters](https://redirectly.app/glossary/utm-parameters.md) - [Mobile Attribution](https://redirectly.app/glossary/attribution.md) ## Related pages - [QR Codes Use Case](https://redirectly.app/use-cases/qr-codes.md) - [Deep Link Generator](https://redirectly.app/tools/deep-link-generator.md) - [UTM Builder](https://redirectly.app/tools/utm-builder.md) --- Full site index for AI agents: https://redirectly.app/llms.txt