B2B apps have deep linking requirements that consumer apps do not: multi-tenant workspaces, SSO authentication flows, IT admin controls, and enterprise security policies. A deep link to a project management task needs to route to the correct workspace, verify the user's permissions, and comply with the organization's security requirements before showing the content.
This guide covers deep linking patterns specific to B2B and enterprise apps. For enterprise onboarding, see onboarding for enterprise mobile apps. For enterprise platform considerations, see enterprise deep linking platforms.

Multi-Tenant Deep Links
Workspace Routing
B2B apps serve multiple organizations. Every deep link needs a workspace context:
/workspaces/{workspace-slug}/projects/{project-id}
/workspaces/{workspace-slug}/tasks/{task-id}
/workspaces/{workspace-slug}/documents/{doc-id}
/{org-slug}/settings/billing
The workspace slug ensures the user is routed to the correct tenant:
func handleDeepLink(_ url: URL) {
let pathComponents = url.pathComponents
guard pathComponents.count >= 3,
pathComponents[1] == "workspaces" || pathComponents[1] == "w" else {
showWorkspacePicker()
return
}
let workspaceSlug = pathComponents[2]
// Check if user has access to this workspace
WorkspaceManager.shared.switchTo(workspaceSlug) { result in
switch result {
case .success:
let contentPath = pathComponents.dropFirst(3).joined(separator: "/")
navigateToContent(contentPath)
case .noAccess:
showAccessDenied(workspaceSlug)
case .notMember:
showJoinWorkspacePrompt(workspaceSlug)
}
}
}
Subdomain-Based Workspaces
Many B2B apps use subdomains per organization:
https://acme.yourapp.com/projects/123
https://contoso.yourapp.com/tasks/456
Each subdomain needs its own Universal Links and App Links verification. To avoid managing hundreds of verification files, use a wildcard pattern:
{
"applinks": {
"apps": [],
"details": [{
"appIDs": ["TEAM_ID.com.yourapp"],
"components": [{
"/": "/projects/*"
}, {
"/": "/tasks/*"
}]
}]
}
}
Serve the same AASA file from all subdomains via a wildcard DNS and web server configuration.
SSO Integration
SAML/OIDC Redirect Flow
Enterprise users authenticate via their identity provider (Okta, Azure AD, Google Workspace). Deep links must handle the SSO redirect:
User taps deep link → https://acme.yourapp.com/tasks/456
→ App opens, user not authenticated
→ Redirect to SSO: https://sso.acme.com/saml?RelayState=tasks/456
→ User authenticates with corporate credentials
→ SSO redirects back: https://acme.yourapp.com/sso/callback?token=xyz&relay=tasks/456
→ App receives token, navigates to tasks/456
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data ?: return
val targetPath = uri.path ?: return
val workspace = extractWorkspace(uri)
if (authManager.isAuthenticated(workspace)) {
navigateTo(targetPath)
} else {
// Store pending deep link, start SSO
pendingDeepLink = targetPath
val ssoUrl = authManager.getSSOUrl(workspace, relayState = targetPath)
startSSOFlow(ssoUrl)
}
}
}
MDM and Managed App Configuration
Enterprise apps deployed through Mobile Device Management (MDM) can receive configuration that affects deep linking:
<!-- Managed App Configuration (iOS) -->
<dict>
<key>workspace_url</key>
<string>https://acme.yourapp.com</string>
<key>sso_provider</key>
<string>okta</string>
<key>allowed_deep_link_domains</key>
<array>
<string>acme.yourapp.com</string>
<string>links.yourapp.com</string>
</array>
</dict>
Enterprise Security for Deep Links
Link Validation
Enterprise apps should validate deep links more strictly than consumer apps:
func validateDeepLink(_ url: URL) -> Bool {
// 1. Check allowed domains
let allowedDomains = ManagedConfig.shared.allowedDomains
guard let host = url.host, allowedDomains.contains(host) else {
log.security("Blocked deep link from unauthorized domain: \(url.host ?? "nil")")
return false
}
// 2. Check for prohibited paths
let blockedPaths = ["/admin", "/settings/delete", "/export"]
if blockedPaths.contains(where: { url.path.hasPrefix($0) }) {
log.security("Blocked deep link to restricted path: \(url.path)")
return false
}
// 3. Validate URL structure
guard url.scheme == "https" else { return false }
return true
}
Audit Logging
Enterprise customers require audit logs for compliance. Log all deep link accesses:
async function logDeepLinkAccess(userId, url, workspace, result) {
await auditLog.create({
eventType: 'deep_link_access',
userId,
workspaceId: workspace.id,
url: url.toString(),
path: url.pathname,
result, // 'success', 'access_denied', 'not_found', 'auth_required'
timestamp: new Date(),
ipAddress: request.ip,
userAgent: request.headers['user-agent']
});
}
Data Loss Prevention
Some enterprises block external sharing. Deep links shared outside the organization should be restricted:
fun canShareDeepLink(workspace: Workspace, targetUser: User?): Boolean {
val sharingPolicy = workspace.securityPolicy.externalSharing
return when (sharingPolicy) {
SharingPolicy.ALLOWED -> true
SharingPolicy.INTERNAL_ONLY -> targetUser?.workspaceId == workspace.id
SharingPolicy.DISABLED -> false
}
}
Notification Deep Links for B2B
Business-Critical Notifications
B2B notifications often carry more urgency than consumer apps:
| Notification | Deep Link | Priority |
|---|---|---|
| Assigned a task | /w/{ws}/tasks/{id} |
Normal |
| Approval requested | /w/{ws}/approvals/{id} |
High |
| Mentioned in comment | /w/{ws}/comments/{id} |
Normal |
| System alert | /w/{ws}/alerts/{id} |
Critical |
| Contract expiring | /w/{ws}/contracts/{id} |
High |
Email Deep Links
B2B apps send many transactional emails. Each should contain a deep link:
<p>You've been assigned a new task:</p>
<p><strong>Q3 Revenue Report</strong></p>
<p>Due: July 15, 2026</p>
<a href="https://acme.yourapp.com/tasks/TASK-789">View Task</a>
Invitation and Onboarding Deep Links
Team Invitation
When inviting a colleague to a workspace:
https://yourapp.com/invite/{invite-token}
The invite link should:
- Work even if the invitee does not have the app or an account.
- Create the account, join the workspace, and land on the relevant content.
- Expire after a configurable period (IT admin setting).
func handleInviteDeepLink(_ url: URL) {
guard let token = url.pathComponents.last else { return }
InviteService.shared.validateToken(token) { result in
switch result {
case .valid(let invite):
if AuthManager.shared.isAuthenticated {
joinWorkspace(invite.workspaceId)
} else {
showSignUpFlow(invite: invite)
}
case .expired:
showError("This invitation has expired. Ask your admin for a new link.")
case .revoked:
showError("This invitation has been revoked.")
}
}
}
Tolinku for B2B Apps
Tolinku supports custom domain routing for B2B apps. Configure workspace-aware routes in the Tolinku dashboard, and Tolinku handles the routing between app and web fallback. The web fallback can redirect to your SSO provider for authentication before showing content.
For enterprise onboarding patterns, see onboarding for enterprise mobile apps.
Get deep linking tips in your inbox
One email per week. No spam.