Docs · Guides and API referenceAPI v1 · SDK 1.0

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.

  1. 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 domain yourapp.hopqo.link is ready right away.

  2. Install the SDK

    npm install @hopqo/react-native
    cd ios && pod install
  3. Initialise and handle links

    Call init once at startup with your app's publishable key, then route whatever arrives in onLink.

    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);
    });
  4. Turn on app links

    Follow the iOS and Android steps to add your link domain. We host the association files for you.

  5. 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. onLink receives /test with isFirstOpen: true, and the install shows under Live in the dashboard.

API keys

Each app has two kinds of key. Find both under Settings → Keys.

KeyPrefixUse it inCan do
Publishablepk_live_Apps and websitesResolve links, report opens and events for that app
Secretsk_live_Your servers onlyEverything in the REST API
Never put a secret key in a mobile app or a web page. If one leaks, roll it under Settings → Keys, and the old key stops working within a minute.

Test-mode keys (pk_test_, sk_test_) work the same but keep data out of your live reports.

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.

  1. The person taps a link. We record the click with its path and params.
  2. The app isn't installed, so they go to the App Store or Play Store.
  3. On first open, the SDK asks Hopqo for a match and gets the original path back.
  4. onLink fires with isFirstOpen: true. Your code routes them.
Register 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.

MethodPlatformResultHow
Install ReferrerAndroidexactGoogle Play passes our click ID to the app through the Play Install Referrer API.
Clipboard hand-offiOSexactThe 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.
ProbabilisticiOSprobableDevice 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 IDWebexactA 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.

App.tsx
Hopqo.track('signup');
Hopqo.track('purchase', { value: 1299, currency: 'INR' });
Hopqo.setUserId('user_8812'); // optional, your own ID

iOS setup

  1. In the dashboard, add your Team ID and bundle ID. We publish apple-app-site-association on your link domains.
  2. In Xcode, open Signing & Capabilities, add Associated Domains, and add applinks:yourapp.hopqo.link plus any custom domains.
  3. Pass incoming universal links to the SDK. The React Native and Flutter packages do this for you; for native apps, forward the URL:
SceneDelegate.swift
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

  1. Add your package name and the SHA-256 fingerprint of your signing key (from Play Console → App integrity). We publish assetlinks.json on your link domains.
  2. Add an intent filter with autoVerify for each link domain:
AndroidManifest.xml
<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.

app.json (Expo)
{
  "expo": {
    "plugins": [["@hopqo/react-native", { "domains": ["yourapp.hopqo.link"] }]]
  }
}

Create a link from the app

Share.tsx
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:

lib/share.dart
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.

index.html
<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.

Authentication
curl https://api.hopqo.com/v1/links \
  -H "Authorization: Bearer sk_live_…"

Create a link

POST/v1/links
FieldTypeDescription
pathrequiredstringWhere to send people inside the app, such as /offers/summer.
paramsobjectData delivered to onLink. Up to 2 KB.
slugstringCustom short path. Random if left out.
domainstringOne of your domains. Defaults to yourapp.hopqo.link.
campaignstringCampaign name for reports.
source, mediumstringUTM-style tags.
fallbackobjectios, android and desktop URLs.
socialobjecttitle, 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"}'

Response

201 Created
{
  "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

GET/v1/links/{id}

Returns the link object above. Use the link's id or its full short URL, URL-encoded.

List links

GET/v1/links?campaign=summer_sale&limit=50

Returns { "data": [...], "next": "cursor" }. Pass next as cursor to get the next page. Filter by campaign, source, domain or created_after.

Link stats

GET/v1/links/{id}/stats?from=2026-10-01&to=2026-10-23
200 OK
{ "clicks": 48310, "installs": { "exact": 17476, "probable": 728 }, "opens": 9120, "events": { "signup": 10812 } }

Send an event from your server

POST/v1/events

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.

Request body
{ "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.

StatusCodeMeaning
400invalid_requestA field is missing or malformed. message names it.
401invalid_keyThe key is missing, rolled or a publishable key was used for a secret-only call.
404not_foundNo link with that ID in this app.
409slug_takenThat slug is already used on this domain.
429rate_limitedSlow down. Retry-After says how many seconds.

Limits

LimitValue
REST API requests100 per second per app
Bulk link creation1,000 links per request
Link params2 KB
Match window7 days by default
Redirects and clicksNot 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.

install.attributed
{
  "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.

verify.js
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

VersionChange
1.0First public release: links, deferred deep links, matching, events, REST API v1 and webhooks.