Troubleshooting
Guides
Symptom, cause and fix for unattributed installs, App Links and Universal Links that do not verify, links that open the browser, 429s, webhook tests, preview auto-fill, Google sign-in and expired links.
Every entry describes what the hosted service and the SDK do today. Where a dashboard message or an API error string is quoted, it is quoted exactly.
An install is not attributed
Section titled “An install is not attributed”Start on the Overview page. The “Match confidence & referrer health” panel shows credited installs, Android referrer coverage (amber when more than 5 % are missing), fingerprint accuracy from the shadow match, click-to-install latency, a confidence bar per platform and the “Install referrer reads” chips. It counts reinstalls and app updates, so its totals differ from the Installs KPI. See Overview.
The confidence hover texts are: Guaranteed install referrer, copied link or paste; High fingerprint, corroborated and unique — credited; Medium routed to the link, not credited; Low ambiguous or contradicted — nothing; No candidate no click from this network in the window.
Read the referrer status chips
Section titled “Read the referrer status chips”Each chip is the result of the SDK’s install-referrer read on Android. Amber chips are the ones to act on.
| Chip | Value | Meaning | What to do |
|---|---|---|---|
| referrer delivered | ok |
The store handed back a referrer. If it carried dtg_click, the install is matched at guaranteed. If not, the install came through the store without a DecTog redirect and the ladder moved on. |
Nothing, unless ok installs are organic: check that the tap went through a tagged store URL (below). |
| store reported organic | organic |
Google Play returned its organic marker or an empty referrer. The install did not come through a tagged store link. | Check the path the tap took. The QR page’s store buttons are untagged, a custom redirect_url or dec_redirect to a non-store host is never tagged, and custom vivo store URLs are never tagged. |
| store too old for the API | feature_not_supported |
The Play Store on the device does not support the Install Referrer API. Final. | Nothing. The fingerprint still runs. |
| store service unavailable | service_unavailable |
No connection to the Play Store service after up to three connection attempts, 3 seconds apart. | The SDK asks again on up to five later launches and sends a late claim; onAppInstalled emits the upgraded verdict. |
| developer error | developer_error |
Play rejected the call after every retry. | Check that the installed build has the Install Referrer client available and is not stripped by your build tooling. |
| permission error | permission_error |
The store refused the read. Final. | Nothing. |
| timed out | timeout |
The read exceeded installReferrerTimeout (default 5 seconds). |
The SDK retries on later launches and claims late. Raise installReferrerTimeout if your first launch is short. |
| read error | error |
Any other failure. | Retried on later launches. Turn on enableDebugLogging to see the exception. |
| no store app | not_available |
No Play Store (or no reader for the store that installed the app). Final. | For Huawei and vivo devices nothing is needed. For Galaxy Store and GetApps add the Samsung or Xiaomi referrer library to the host app. |
| iOS (no referrer API) | not_android |
Expected on iOS. | iOS attribution uses the pasteboard and the fingerprint. |
| not reported (older SDK) | unknown |
The app did not send a status. | Update the app to the current SDK. |
“Missing” in the coverage figure counts service_unavailable, timeout, error and not_available.
The click was never recorded
Section titled “The click was never recorded”- Bots and link unfurlers get the preview page and no click: any user agent containing
bot(as a word),crawler,spider,preview,fetcher,monitor,headless,whatsappor the named social crawlers, and any request Cloudflare scores below 30. A plaincurlis not on the list; unless Cloudflare scores it below 30, it records a click. - Hint-less Chromium: the first request from a mobile Chromium browser that sends
Sec-CH-UA-Mobile: ?1without aSec-CH-UA-Modelheader, has noBuild/token in its user agent and no in-app marker, gets a 302 withCritical-CHand records nothing. Chrome retries with the hint and the retry records the click. A client that never retries leaves no click row. The install can still be matched: the Play and OEM store referrers carrydtg_l=<username>/<slug>, and the server synthesizes the click from it. With the mobile interstitial on, the same happens through a one-shot self-redirect carrying?_ch=1. - In-app browsers (Facebook, Instagram and WebViews on Android) never take the hint path and never get the intent bounce. They get a plain 302 to the tagged store URL, so referrer attribution still works for them.
- The demo seed’s installs carry no IP hash and never fingerprint-match. Its attributed installs are all Android referrer matches and its organic ones all iOS. Do not judge the fingerprint with the demo account.
The fingerprint found nothing
Section titled “The fingerprint found nothing”- Candidates are web clicks from an iOS or Android user agent with the same hashed IP and the same OS family as the install that no other install has claimed. Clicks the SDK logged from inside the app and desktop clicks are never candidates.
- The window is 24 hours only when the click and the install share a device model or a screen profile to compare and the install is not on cellular. Phone browsers send no model, so on iOS the screen profile the interstitial or tap-to-store page records is what earns the long window. Everything else gets 2 hours, or the low-entropy window of the install’s own app (1 to 720 hours), never more than the wide window. Without the device tier the install carries no model or screen data and only ever gets the short window.
mediumconfidence returns aroutingcandidate but no credit.lowreturns nothing. See How attribution works.- One install per click. A click another install already consumed moves the ladder on;
click_reuseis flagged only when nothing later matches. - App updates (
install_state: update) never run the fingerprint; the panel’s “kept out of matching” line counts them. Reinstalls (is_reinstall) run the full ladder and the fingerprint. They are left out of the Installs KPI and the per-link counts, not out of matching, so a reinstall can hold a click that the install you are looking for needed.
Strict fraud mode made it organic
Section titled “Strict fraud mode made it organic”With strict fraud mode on (Apps, Advanced), an install that trips any rule (emulator, impossible_speed, install_velocity_ip, click_storm_ip, stale_referrer, stale_clipboard, stale_meta, click_reuse) is stored with matched: false and match_source: null. It keeps match_confidence, referrer_source and matched_slug on the record; the verdict and the webhook send the first two but never the slug. The click stays consumed. Strict mode is read from the app the install concerns: the credited link’s app, else the app whose package the SDK reports. The fraud box on the Overview and the chips on a customer’s detail page show the rules that fired.
iOS specifics
Section titled “iOS specifics”- The pasteboard read is on by default. It only attributes when the link was on the pasteboard: turn on iOS tap-to-store (Apps, Advanced) so the “Get the app” button copies the link before opening the App Store. See Deterministic attribution.
- On iOS 16 and later the read shows the system “Allow paste from …?” alert when the pasteboard holds a URL. A user who declines leaves the install on the fingerprint.
willShowPasteboardPrompt()tells you in advance;DectogPasteButtonlets the user paste later and sends a late claim. - Simulators cannot install from the App Store and do not open Universal Links. Test on a device.
Check the SDK side
Section titled “Check the SDK side”onAppInstalledemits once per install, plus once more when a late claim upgrades the verdict.currentAppInstallholds the latest verdict. To retest, uninstall the app; on Androidadb shell pm clear <package>also works. The SDK keeps its state in.dectog_install.jsonin a backup-excluded directory and validates it against the package’s first-install time, so a restored backup does not suppress a real install.- Set
enableDebugLogging: trueand look for the install POST. A 401 means the key was regenerated or is wrong; the server’s four 401 bodies are listed under Automation and agents. A 429 is retried afterRetry-After(up to 10 seconds in-process, otherwise on the next launch).
Check the app against the dashboard
Section titled “Check the app against the dashboard”Call FlutterDectog().diagnostics() from a debug build. It reads the app’s record from the server and reports a bundle ID or package that differs, an iOS URL scheme missing from Info.plist, and on Android 12 and newer each link host the system has not verified for App Links (none means verification failed or never ran). With enableDebugLogging the warnings are also printed.
App Links do not verify (Android)
Section titled “App Links do not verify (Android)”| Symptom | Cause | Fix |
|---|---|---|
| Taps open Chrome or the store although the app is installed | The domain is not verified for the installed build | adb shell pm get-app-links <package> must list {username}.dectog.com as verified. Reinstall after changing the manifest or the fingerprints; Android verifies at install and update. |
Verify shows N package(s), MISSING fingerprints |
No fingerprint saved for the package | Add the SHA-256 fingerprints on the Apps page. |
| Verification fails although a fingerprint is saved | The saved fingerprint is not the one that signed the installed build | Store builds are signed by the Play App Signing key. Copy it from Play Console (Setup, App signing) and add it next to your upload and debug keys. Fingerprints are 32 colon-separated hex pairs; the server stores them uppercase. |
| Only some hosts verify | The intent filter uses a wildcard | Intent filters do not support wildcards. Add an android:autoVerify="true" filter per host and list {username}.dectog.com explicitly. |
| Verification never succeeds in development | Dev hosts (10.0.2.2, localhost) cannot serve an assetlinks file Android accepts |
Expected. Android 12 and later open those links only from explicit intents (adb shell am start) or after the user enables the link in system settings. |
Verify shows Endpoint replied 404: Configuration not found |
No app on the account has a package | Save the package first. The well-known files are account-wide and built from every app. |
Until verification succeeds, taps from a Chrome-engine browser (user agent contains Chrome/) still reach the app through an intent:// bounce, provided the app has both a scheme and a package. In-app browsers never get the bounce. See Platform setup.
Universal Links do not open the app (iOS)
Section titled “Universal Links do not open the app (iOS)”| Symptom | Cause | Fix |
|---|---|---|
Verify shows No app IDs configured or Endpoint replied 404: Configuration not found |
The AASA is built only from apps that have both a bundle ID and a Team ID | Save both on the Apps page. The file then serves at https://{username}.dectog.com/.well-known/apple-app-site-association and includes webcredentials. |
| Taps open Safari although the file verifies | The entitlement is missing or the app was not reinstalled | Add applinks:{username}.dectog.com to the Associated Domains entitlement of every build configuration, enable the capability on the App ID, then reinstall. iOS fetches the AASA at install and update; an installed app never picks up a change. |
The entitlement is a wildcard, applinks:*.dectog.com |
For a wildcard entry iOS fetches the AASA from the parent domain, https://dectog.com/.well-known/apple-app-site-association, and the apex answers 400 {"error":{"code":"invalid_request","message":"Subdomain not found in host"}} |
Use the explicit host: applinks:{username}.dectog.com. |
| Works from Notes but not from the address bar | Typing a URL into Safari never triggers a Universal Link | Tap the link from Notes, Messages or another app. |
| Nothing works locally | The AASA must be served from a real domain over HTTPS | Path-mode and localhost deployments cannot pass; test against a deployed domain. The Simulator does not open Universal Links either. |
A redirect to myapp:// does nothing |
iOS never opens a custom scheme from a server redirect | Custom schemes open only from a direct tap: the “Open in app” button of the tap-to-store page or the buttons on the QR page. |
Links open the browser or the store instead of the app
Section titled “Links open the browser or the store instead of the app”- The domain is not verified for the installed build: see the two sections above.
- The tap came from an in-app browser. Android in-app browsers and WebViews get a plain 302 to the store, never the intent bounce. On iOS the tap-to-store page leads with “Open in app” inside Facebook, Instagram, Messenger, LINE, WeChat, Snapchat, TikTok and X when the app has an iOS scheme; otherwise the visitor gets the store.
- The visitor is on a desktop user agent. Desktop never redirects to the target; it gets the QR page or a fallback URL. An iPad asks for desktop sites with a Mac user agent; its Safari tap counts as iOS only when the link’s app is set up for iOS (a bundle ID, App Store URL or iOS scheme). See How a tap is routed.
- The SDK reports
Invalid DecTog URL format: the tapped host does not matchbaseUrlandlinkRootDomain. On the hosted service leave both unset. - The app has no store URL: a phone without the app goes to the app’s fallback URL, then the link target, then the QR page. Add the store URLs on the Apps page.
429 Too Many Requests
Section titled “429 Too Many Requests”| Limit | Scope | Behavior |
|---|---|---|
| 3000 per minute | The account | {"error":{"code":"rate_limited","message":"Rate limit exceeded"}} with Retry-After |
| 60 per minute | A device_id (SDK calls) |
Same |
| 600 per minute | The account plus the caller’s IP, for calls without a device_id (every link call from a script) |
Same |
| 120 per minute | The interstitial beacon POST /_/collect, per IP |
429 with an empty body; this limiter also returns 429 when its database write fails |
| 5 per hour | The contact form, per IP | Too many messages — try again later, no Retry-After |
Retry-After is the number of seconds until the current one-minute window ends, at least 1. The hosted service, and a deployment set up with npm run setup, count the account, device, IP and beacon limits with Cloudflare’s Rate Limiting bindings, per Cloudflare location and approximately; without the bindings the counters live in D1, in windows aligned to the clock, not sliding. When the limiter’s binding or database fails, SDK and API requests go through. The SDK honors Retry-After up to 10 seconds in-process and defers longer waits to the next launch; event batches rejected with 429 stay queued. Scripts should sleep for Retry-After seconds. See API overview.
Webhooks not arriving
Section titled “Webhooks not arriving”Open the app on the Apps page, then Deliveries on the webhook:
| What the log shows | Meaning |
|---|---|
| Nothing for an event you expected | The webhook is not subscribed to it, or takes another app’s events (an install belongs to the credited link’s app, else the install’s own app, else the oldest app). A webhook added in the last 30 seconds may not have reached every server yet. |
Retry {time} with HTTP 500, HTTP 401 … |
Your endpoint answered but not 2xx; the response start is in the last column. A 401 from your own check usually means the body was parsed and re-serialized before verifying, or the secret was rotated. |
Retry with a network error or The operation was aborted |
The endpoint could not be reached, or took more than 10 seconds |
| Failed | Six attempts over about a day failed; Resend tries again now |
| Status chip Off: 20 failed deliveries in a row | The webhook was turned off; fix the endpoint, then Turn on |
Send test delivers a webhook.test event, logged like the others. See Webhooks.
Social preview: “Auto-fill from fallback URL”
Section titled “Social preview: “Auto-fill from fallback URL””The button fills empty preview fields from the fallback URL’s Open Graph tags. It follows redirects, waits 8 seconds, reads at most 512 KB and only fetches public hosts.
| Message | Meaning |
|---|---|
Enter a valid http(s) URL |
The value is not a URL |
Enter a valid public http(s) URL |
Not http or https, or a private, loopback or link-local host (localhost, .local, .internal, 10., 127., 192.168., 172.16-31., 169.254., 100.64-127., ::1, fc, fd, fe80) |
Couldn't reach that URL |
Connection failure or timeout |
That URL returned HTTP 503 |
The page answered with a non-2xx status |
That URL isn't an HTML page |
The Content-Type is not HTML or XML |
No title, description or image found on that page |
The page has no usable og:*, twitter:*, <title> or description tags |
Titles and descriptions are cut to 150 characters. See Social previews and QR.
Google sign-in
Section titled “Google sign-in”| Message | Meaning |
|---|---|
Google sign-in was cancelled. |
You closed Google’s consent screen |
Google sign-in timed out — please try again. |
The OAuth state cookie (10 minutes) was missing or did not match when Google sent you back |
Couldn't verify your Google account — please try again. |
Google’s token could not be exchanged or checked, or the Google email is not verified |
No DecTog account uses that Google email, and signup is currently closed. |
Signups are closed and no account has that email |
That email is already linked to a different Google account. |
The account with that email is linked to another Google identity |
Google sign-in is not available right now. |
Google sign-in is not configured on this deployment |
A Google sign-in whose verified email matches an existing account links that account automatically. New Google users have 15 minutes to choose a username; after that the username step answers Google sign-in expired — try again and you start over. Google-only accounts have no usable password; set one with “Forgot password?”. Reset links are valid for one hour, work once, and a new request cancels older ones: This reset link is invalid or has expired, This reset link has already been used. See Account and sign-in.
404 and 410 pages
Section titled “404 and 410 pages”| Response | When | Page text |
|---|---|---|
302 to https://dectog.com/ |
The username does not exist | None (hosted service) |
| 404 page | The slug does not exist, or its link was deleted (an expired temporary link is, a day after expiry) | Link not found and This DecTog link does not exist. |
| 410 page | A temporary link has expired, bots included | Link expired and This temporary link has expired and is no longer available. |
JSON 404 {"error":{"code":"not_found","message":"Not found"}} |
Two or more path segments on a link host | None |
Both HTML pages carry noindex, a “Go to DecTog” button and the footer Check the link for typos, or ask whoever shared it for a new one.
An expired link answers GET /api/v1/resolve/{username}/{slug} with 410 link_expired, and a paused one with 410 link_paused; neither records clicks. The daily job deletes temporary links expired for more than 24 hours that have no expired URL; after that the link is a 404, and its slug stays reserved until you restore it or delete it permanently. Deleting a link from the dashboard hides it and keeps its numbers in the Overview; only Delete permanently removes its clicks, installs, events and opens. See Temporary links.