Docs
Build with Hopqo
Hopqo gives your app links that open a specific screen, even when the person has to install the app first, and tells you which link brought each install. This guide gets you from zero to a working deferred deep link, then covers each platform and the REST API.
Quickstart
You'll create an app in the dashboard, install the SDK, turn on app links and test a link on a real phone.
Create an app
Sign up at
app.hopqo.com, choose New app, and enter your iOS bundle ID, Apple Team ID and Android package name with its SHA-256 signing fingerprint. Your link domainyourapp.hopqo.linkis ready right away.Install the SDK
npm install @hopqo/react-native cd ios && pod install
flutter pub add hopqo_flutter
npm install @hopqo/web
Initialise and handle links
Call
initonce at startup with your app's publishable key, then route whatever arrives inonLink.import { Hopqo } from '@hopqo/react-native'; Hopqo.init({ key: 'pk_live_…' }); Hopqo.onLink((link) => { // link.path '/p/runner-x' // link.params { code: 'SUN20' } // link.isFirstOpen true after an install navigation.navigate(link.path, link.params); });
import 'package:hopqo_flutter/hopqo.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Hopqo.init(key: 'pk_live_…'); Hopqo.onLink.listen((link) => router.go(link.path, extra: link.params)); runApp(const App()); }
import { Hopqo } from '@hopqo/web'; const hq = Hopqo.init({ key: 'pk_live_…' }); // Turn any button into an "Open in app" deep link hq.attach('#open-in-app', { path: location.pathname });
Turn on app links
Follow the iOS and Android steps to add your link domain. We host the association files for you.
Test a link
Create a link in the dashboard with the path
/test. Uninstall the app, tap the link on your phone, install from the store or from Xcode, and open the app.onLinkreceives/testwithisFirstOpen: true, and the install shows under Live in the dashboard.
API keys
Each app has two kinds of key. Find both under Settings → Keys.
| Key | Prefix | Use it in | Can do |
|---|---|---|---|
| Publishable | pk_live_ | Apps and websites | Resolve links, report opens and events for that app |
| Secret | sk_live_ | Your servers only | Everything in the REST API |
Test-mode keys (pk_test_, sk_test_) work the same but keep data out of your live reports.
Links and domains
A link has a short URL, a destination path inside your app, optional parameters, and fallbacks for each platform.
| Part | Example |
|---|---|
| Short URL | https://inxits.hopqo.link/summer-sale |
| Path | /offers/summer |
| Params | { "code": "SUN20" }, up to 2 KB |
| Fallbacks | iOS → App Store, Android → Play, desktop → https://inxits.com/summer |
| Campaign tags | campaign, source, medium, content |
Custom domains
On Pro and above, add a domain such as go.inxits.com under Settings → Domains and create the CNAME record we show you. We issue the TLS certificate and serve the association files on it automatically.
Deferred deep links
A normal deep link only works if the app is already installed. A deferred deep link remembers where the person was going while they install, and delivers it on the first open.
- The person taps a link. We record the click with its path and params.
- The app isn't installed, so they go to the App Store or Play Store.
- On first open, the SDK asks Hopqo for a match and gets the original path back.
onLinkfires withisFirstOpen: true. Your code routes them.
onLink before your first screen renders, so a deferred link isn't missed while the app loads.How matching works
Every install is matched to a click by the strongest method available. The method and a score are stored on the install and shown in reports and webhooks.
| Method | Platform | Result | How |
|---|---|---|---|
| Install Referrer | Android | exact | Google Play passes our click ID to the app through the Play Install Referrer API. |
| Clipboard hand-off | iOS | exact | The landing page copies a short one-time token when the person taps Get the app. The SDK reads it once on first open and clears it. |
| Probabilistic | iOS | probable | Device model, OS version, network and time since click are scored from 0 to 1. Matches below your threshold (default 0.85) are dropped. |
| First-party ID | Web | exact | A click ID on your own domain, read by the web SDK. |
The match window is 7 days from the click by default and can be shortened per app. iOS shows a paste notice when the SDK reads the clipboard. You can turn the hand-off off and rely on probabilistic matching only.
Events and funnels
Send events from the app to see what each link's users do after installing. Events are tied to the install automatically.
Hopqo.track('signup'); Hopqo.track('purchase', { value: 1299, currency: 'INR' }); Hopqo.setUserId('user_8812'); // optional, your own ID
iOS setup
- In the dashboard, add your Team ID and bundle ID. We publish
apple-app-site-associationon your link domains. - In Xcode, open Signing & Capabilities, add Associated Domains, and add
applinks:yourapp.hopqo.linkplus any custom domains. - Pass incoming universal links to the SDK. The React Native and Flutter packages do this for you; for native apps, forward the URL:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard let url = userActivity.webpageURL else { return } Hopqo.handle(url) }
The iOS landing page
When the app isn't installed, iOS visitors see a short page with your icon and a Get the app button that starts the clipboard hand-off. Customise its colors and text under Settings → Landing page. On Free it shows a small Hopqo mark.
Android setup
- Add your package name and the SHA-256 fingerprint of your signing key (from Play Console → App integrity). We publish
assetlinks.jsonon your link domains. - Add an intent filter with
autoVerifyfor each link domain:
<intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="yourapp.hopqo.link" /> </intent-filter>
The SDK reads the Play Install Referrer on first open, so Android installs match exactly. It needs the com.android.installreferrer library, which the SDK pulls in.
React Native
Works with the bare workflow and Expo (with a config plugin). Supports React Native 0.72 and later.
{
"expo": {
"plugins": [["@hopqo/react-native", { "domains": ["yourapp.hopqo.link"] }]]
}
}Create a link from the app
const { url } = await Hopqo.createLink({ path: `/recipes/${recipe.id}`, social: { title: recipe.name, image: recipe.photo }, }); Share.share({ message: url });
Flutter
Supports Flutter 3.16 and later, on iOS and Android. Add the associated domain and intent filter from the iOS and Android steps above, then:
final link = await Hopqo.createLink( path: '/recipes/${recipe.id}', social: SocialMeta(title: recipe.name), ); Share.share(link.url);
Web
The web SDK is under 6 KB gzipped. Use it for "Open in app" buttons, smart banners and first-party click IDs on your own site.
<script src="https://cdn.hopqo.com/web/1.0/hopqo.min.js"></script> <script> Hopqo.init({ key: 'pk_live_…' }).banner({ title: 'inXits is better in the app' }); </script>
REST API
The base URL is https://api.hopqo.com/v1. Send your secret key as a bearer token. Requests and responses are JSON.
curl https://api.hopqo.com/v1/links \
-H "Authorization: Bearer sk_live_…"Create a link
| Field | Type | Description |
|---|---|---|
| pathrequired | string | Where to send people inside the app, such as /offers/summer. |
| params | object | Data delivered to onLink. Up to 2 KB. |
| slug | string | Custom short path. Random if left out. |
| domain | string | One of your domains. Defaults to yourapp.hopqo.link. |
| campaign | string | Campaign name for reports. |
| source, medium | string | UTM-style tags. |
| fallback | object | ios, android and desktop URLs. |
| social | object | title, description, image for previews. |
curl https://api.hopqo.com/v1/links \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{"path":"/offers/summer","params":{"code":"SUN20"},"slug":"summer-sale","campaign":"summer_sale"}'
const res = await fetch('https://api.hopqo.com/v1/links', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HOPQO_SECRET}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ path: '/offers/summer', params: { code: 'SUN20' }, slug: 'summer-sale' }), }); const link = await res.json();
import os, requests link = requests.post( "https://api.hopqo.com/v1/links", headers={"Authorization": f"Bearer {os.environ['HOPQO_SECRET']}"}, json={"path": "/offers/summer", "params": {"code": "SUN20"}, "slug": "summer-sale"}, ).json()
Response
{
"id": "lnk_01JB8Q4X2T",
"url": "https://inxits.hopqo.link/summer-sale",
"path": "/offers/summer",
"params": { "code": "SUN20" },
"campaign": "summer_sale",
"qr": "https://api.hopqo.com/v1/links/lnk_01JB8Q4X2T/qr.svg",
"created_at": "2026-10-23T18:40:00Z"
}Get a link
Returns the link object above. Use the link's id or its full short URL, URL-encoded.
List links
Returns { "data": [...], "next": "cursor" }. Pass next as cursor to get the next page. Filter by campaign, source, domain or created_after.
Link stats
{ "clicks": 48310, "installs": { "exact": 17476, "probable": 728 }, "opens": 9120, "events": { "signup": 10812 } }Send an event from your server
For events that happen on your backend, such as a payment clearing. Identify the person by the user_id you set with setUserId, or by install_id.
{ "name": "first_sip", "user_id": "user_8812", "value": 500, "currency": "INR" }Errors
Errors return a JSON body with a stable code and a readable message.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or malformed. message names it. |
| 401 | invalid_key | The key is missing, rolled or a publishable key was used for a secret-only call. |
| 404 | not_found | No link with that ID in this app. |
| 409 | slug_taken | That slug is already used on this domain. |
| 429 | rate_limited | Slow down. Retry-After says how many seconds. |
Limits
| Limit | Value |
|---|---|
| REST API requests | 100 per second per app |
| Bulk link creation | 1,000 links per request |
| Link params | 2 KB |
| Match window | 7 days by default |
| Redirects and clicks | Not limited |
Webhooks
On Pro and above, add an HTTPS endpoint under Settings → Webhooks. We send install.attributed, open.reengaged and event.tracked. Failed deliveries are retried with backoff for 24 hours.
{
"type": "install.attributed",
"created_at": "2026-10-23T18:44:12Z",
"data": {
"install_id": "ins_01JB8R", "platform": "ios",
"match": { "method": "clipboard", "result": "exact", "score": 1 },
"link": { "id": "lnk_01JB8Q4X2T", "campaign": "summer_sale" }
}
}Verify signatures
Every request carries X-Hopqo-Signature: t=<unix time>,v1=<hex>. Compute HMAC-SHA256 of t + "." + raw body with your webhook secret and compare in constant time. Reject requests older than 5 minutes.
import crypto from 'node:crypto'; export function verify(rawBody, header, secret) { const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('='))); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1)); }
Changelog
| Version | Change |
|---|---|
1.0 | First public release: links, deferred deep links, matching, events, REST API v1 and webhooks. |