Appearance
iOS Universal Links
FunnelsGrove v1 uses one namespaced URL for funnel, email, and direct-campaign traffic:
text
https://go.funnelsgrove.com/<app-key>/<slug>Before integrating the app, complete the project's single iOS app settings: Application Identifier Prefix, Bundle ID, Apple Developer Team ID, App Store URL, and Universal Links enablement. FunnelsGrove generates the app key; create at least one enabled link. FunnelsGrove serves the matching AASA document at:
text
https://go.funnelsgrove.com/.well-known/apple-app-site-associationAdd this exact value to the app target's Associated Domains entitlement:
text
applinks:go.funnelsgrove.comAn installed app receives the URL as an iOS Universal Link. Without the app installed, the resolver records the click and returns a direct 302 to the project's configured App Store URL. An anonymous first launch after installation remains unattributed unless a deterministic email or account handoff exists.
Receive And Record The Link
Handle NSUserActivityTypeBrowsingWeb, require the go.funnelsgrove.com host, and parse exactly two path segments: appKey and slug. Treat context and handoff query values as opaque; do not decode them or use them as identity.
swift
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
guard let url = activity.webpageURL,
url.host == "go.funnelsgrove.com"
else { return }
let parts = url.pathComponents.filter { $0 != "/" }
guard parts.count == 2 else { return }
let appKey = parts[0]
let slug = parts[1]
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
let context = queryItems?.first {
$0.name == "context" || $0.name == "handoff"
}?.value
let openId = UUID().uuidString
// POST appKey, slug, openId, and optional opaque context as shown below.
}Record the installed-app open with the strict public request:
http
POST https://go.funnelsgrove.com/v1/app-link-opens
Content-Type: application/json
{
"appKey": "<app-key>",
"slug": "<slug>",
"openId": "<client-uuid>",
"context": "<optional-opaque-context-or-handoff-token>"
}context is optional. The public request accepts only appKey, slug, openId, optional context, and allowlisted campaign fields. It must not contain email, user identity, project ID, a project SDK secret, or an attribution admin secret.
json
{
"clickId": "<click-id>",
"destination": "onboarding/welcome"
}Persist the openId until the record request succeeds, then persist the returned clickId until login completes. Route the app using only the returned destination. Retrying the same normalized payload with the same openId returns the same response. Reusing that openId with changed input returns an idempotency conflict.
Bind After Verified Login
After its own verified login, the customer backend—not the app—binds the exact click to the verified email. Set FUNNELSGROVE_INTEGRATION_API_BASE_URL to the Integration API base URL shown in the project integration guide:
http
POST <FUNNELSGROVE_INTEGRATION_API_BASE_URL>/integration/v1/app-attribution/bind-click
Content-Type: application/json
x-sdk-secret-key: <project-sdk-secret-key>
{
"clickId": "<click-id>",
"verifiedEmail": "person@example.com"
}A successful call returns { "attributed": true }.
The customer backend sends the existing project SDK secret to the FunnelsGrove control plane. The control plane derives the project and is the only component that talks to the attribution service with its admin credential. Never embed either secret in a native app or browser.
Retrying the bind with the same normalized email returns the same idempotent { "attributed": true } SDK response while the underlying attribution claim is reused. A different email is not an idempotent retry and fails. If an opaque email handoff created a pending claim, complete that handoff flow instead of exact binding.
Delivery And Rollout
- Configure email providers not to rewrite
go.funnelsgrove.combehind a tracking hostname; a rewritten host will not match the Associated Domains entitlement. - Allow up to 24 hours for Apple CDN propagation. Devices refresh associated-domain data roughly weekly, so existing installs may update later than fresh installs.
- Test both an installed app and an uninstalled device. A local HTTP test proves the resolver contract, not real-device Universal Link delivery.
V1 intentionally excludes Android App Links, Play Install Referrer, deferred attribution, a native SDK, fingerprinting, and probabilistic attribution.