Documentation
Testing it before you ship
Prove a deferred install is matched to the right link BEFORE your app reaches the App Store — through TestFlight and through a local build — with a live view of every click and match as it happens.
The problem with testing this
An installed app opening a link is easy to test: tap, watch it open. A deferred install is not, because the real journey runs through the App Store — and your app is not on the App Store yet, which is the whole reason you are testing.
Why it works anyway, without the App Store
This is the part worth reading before you start, because it tells you which shortcuts are safe.
We never find out where an install came from, and we never ask. There is no App Store receipt check, no purchase history, and on iOS no install referrer — Apple provides nothing of the kind. What our matching actually does is join a recorded click to a first launch of your app, using signals both of them carry.
So the mechanism has exactly two inputs, and neither of them is the App Store:
- A click, recorded on our servers. That happens when somebody taps your link and reaches our page. It has nothing to do with your app at all.
- A launch that the SDK can tell is the first one. The SDK writes a marker into your app’s own storage on every launch. If the marker is absent, this launch is the first — and only then does the SDK ask us whether there is a click to match.
An install from TestFlight, or a build run straight from Xcode, produces exactly the same two inputs as an install from the App Store. The code path is not similar to the production one; it is identical, because nothing in it can distinguish where the binary came from.
The one thing that follows from this, and it is the thing people get wrong: what makes a launch “fresh” is your app’s storage being gone, not the app being reinstalled. Deleting the app from the home screen deletes its storage container, which is why deleting works. Re-running from Xcode over the top of an existing install does not, and is the single commonest reason a test appears to fail.
Open the live view first
Everything below is easier with Apps → your app → Live view open on a laptop. It shows each click arriving, which route resolved it, and whether the install was matched or refused, with the confidence. It refreshes every five seconds.
Check the environment selector matches the link you are tapping. Looking at live while tapping a test link is the commonest reason people think nothing happened — the two are separate worlds and neither can see the other.
Method 1 — TestFlight
The closest thing to the real journey, and what we would use. An internal tester build needs no App Review, so the loop is minutes rather than days.
- Upload a build and add yourself as an internal tester. Install it from TestFlight once, so you know the build runs.
- Delete the app from the phone. Press and hold, remove. This is what clears the storage, and it is what makes the next launch a first launch.
- On the phone, tap one of your links. Let it go through to whatever it offers — the destination does not matter here, and you do not need to install anything from it. The click is recorded the moment our page loads. Confirm it in the live view before going on. If it is not there, stop: everything after this depends on it.
- Install from TestFlight again.
- Open the app. Watch the live view: an install should appear within a second or two — attributed or refused, with a reason either way.
What this proves: the whole deferred path — click recorded, fresh install, matched, parameters delivered. What it does not prove: anything about your App Store listing page, or about install referrer behaviour on Android.
Method 2 — a local build from Xcode
Faster, and it uploads nothing. An install from Xcode is a genuinely fresh install as far as the SDK is concerned, for the reason above: deleting the app deletes the storage, and that storage is the only thing that would otherwise tell us this launch is not the first.
- Delete the app from the phone. Not “stop and re-run” — delete it from the home screen. Xcode installing over the top of an existing app leaves the storage in place, so the launch is an ordinary one and the SDK correctly does nothing.
- Tap your link on the phone. Confirm the click in the live view.
- Build and run to the device from Xcode.
- Watch the live view for the install.
What to reset between attempts
Each attempt needs a genuine first launch, and a second try on an app you did not delete is not one — the SDK will do nothing at all, correctly, and the live view will show nothing.
- On a phone: delete the app. That is the whole procedure, and it is reliable because iOS removes the app’s storage container with it. Nothing else needs clearing.
- Do not reuse a click. Tap the link again for each attempt. A click is consumed by the match it produces, so a second install has nothing to match against and will correctly report no match.
- In a browser, if you are testing the web path: the same two values live in
localStorage, underquberoute.install.v1andquberoute.launched.v1. Clearing site data, or removing those two keys, is the equivalent of deleting the app. A private window works too, and is quicker.
Those key names are internal and may change between versions — they are given here because they are useful while testing, not as something to build on.
What you should see in the live view, at each stage
Rows arrive newest first. Each one says what kind of thing it was, how long ago, and — for an install — whether we matched it and how sure we are.
- After tapping the link: a
CLICKrow, with the alias and the platform. If the app is already installed and the universal link works, you will not see a click at all — iOS opened your app without ever contacting us. That is success, not failure. - After opening a freshly installed app: an
INSTALLrow. Attributed rows readATTRIBUTED · Certain,· High,· Mediumor· Low. Deferred matching is an inference, so anything below Certain is normal and expected here — Certain is reserved for an install we did not have to guess about. - A
NO MATCHrow is a working system, not a broken one. It means the install arrived, we looked, and we would rather tell you we found nothing than attach it to a click we are not confident about. The row says which it was.
If you see nothing at all after opening the app, in the order each is actually the cause: the environment selector does not match the link; the app was not really deleted, so this was not a first launch; the app was opened by the link directly, in which case there is a CLICK row and no install to defer; or the device had no network at the moment of launch, in which case the SDK will try again on the next launch.
If clipboard matching is switched on
It is off by default. If you have turned it on for this app, the first launch of a fresh install behaves slightly differently and it is worth knowing what you are looking at.
- The SDK asks us first, without reading anything. We reply telling it this app wants the clipboard checked.
- iOS shows your user a prompt — “Allow ‘Your App’ to paste from ‘Safari’?” — with Allow and Don’t Allow. This is the system asking, not us, and it happens at most once in the life of an install.
- The SDK tells us what it found, and we run the match either way. Refusing costs nothing: we are told the clipboard was checked and empty, and ordinary deferred matching carries on. You should still see an install row.
So when testing: expect the paste prompt on the first launch after a fresh install, expect it not to appear on subsequent launches, and try it both ways — allow it once, refuse it once — because both are journeys your users will take. What the prompt looks like, and what it is worth.
qr.onLink(link => {
console.log(link.source) // 'deferred'
console.log(link.confidence) // below 1 — see the deferred matching page
console.log(link.alias) // the link you tapped
console.log(link.params) // your own fields from the URL
})The step-by-step plan
Work down it. Tick each one. Anything that does not match, stop there.
- An installed app opens a link. Tap a link with the app installed. It should open, and
onLinkshould fire withsource: 'opened'andconfidence: 1. - The click appears in the live view — unless the app opened directly, in which case it will not, and that is correct. A working universal link never reaches our servers.
- Your own fields arrive. Add
?visitor_id=TEST1to a link. Tap it. Checklink.params.visitor_idisTEST1. - A deferred install attributes. Method 1 or 2 above.
- The fields survive the deferred path too. The same
visitor_id, on the install rather than the click. This is the one to check if you pay affiliates. - A refusal is legible. Delete the app, install it without tapping any link first, and open it. The live view should show an install that was not attributed, and say why. A blank is a bug; a clear “no clicks to match against” is the system working.
- An event lands. Call
qr.track('purchase', { value: 1, currency: 'GBP' })and check it appears in your report.
If nothing appears at all
In the order each is actually the cause:
- The environment selector does not match the link. By a distance the commonest.
- The universal link worked, so nothing reached us. Your app opened — that is success.
- The app was not really deleted, so this is not a first launch.
- Apple has cached an old association file. It can take up to 24 hours, and the troubleshooting page explains how to tell.
Ask the documentation
It answers from these pages only, and links what it used. If the answer is not here it says so rather than guessing — then email [email protected].