SDK
Using Appy without the SDK
The Appy SDKs are thin clients over two HTTP endpoints. Anything they do, you can do yourself from React Native, Flutter, Unity, a web view or plain platform code.
Everything on this page uses the app’s publishable key (appy_pk_…), which is safe to ship in the app. The Client API, like the SDKs, is part of the Enterprise plan. Calls from your backend use a secret key (appy_sk_…), which never leaves your servers.
The whole protocol
| When | Call | What you get |
|---|---|---|
| Every cold start | POST /v1/sdk/open with firstLaunch | On the first launch after install: the link the user tapped before installing, if any, and the install’s attribution. |
| The app is opened by a URL | POST /v1/sdk/open with url | The link behind that URL, with its parameters. |
| Something worth measuring happens | POST /v1/sdk/events | Events and revenue credited to the link that brought the install. |
Rules that keep the numbers right:
- Generate a random
installId(a UUID) on first launch and keep it until the app is uninstalled. - Send
firstLaunch: trueuntil one open request withfirstLaunch: truehas succeeded (2xx), thenfalseforever. Retrying a first launch is safe; Appy answers it with the same link. - On Android, on the first launch, read the Google Play Install Referrer and send it as
installReferrerwithinstallReferrerSource: "google_play". The referrer carries the link, so Appy knows exactly which link brought the install. Apps installed from Huawei AppGallery can send its referrer withinstallReferrerSource: "huawei_appgallery". - Give every event a unique
idand keep unsent events until the server answers 2xx. A resent event with the sameidis counted once. - If the user opts out, send
"trackingEnabled": false. The server then stores nothing and only resolves URLs you pass explicitly.
Full request and response schemas are in the API reference under Client API.
Try it with curl
curl https://api.appy.to/v1/sdk/open \
-H "Authorization: Bearer $APPY_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"installId": "7f0c2b9e-3a61-4c2e-9d0b-5c1e8f2a9b10",
"firstLaunch": false,
"url": "https://acme.appy.to/spring?item=42",
"device": {"platform": "ios", "osVersion": "17.5"}
}'{
"installId": "7f0c2b9e-3a61-4c2e-9d0b-5c1e8f2a9b10",
"firstLaunch": false,
"deepLink": {
"url": "https://acme.appy.to/spring?item=42",
"deeplinkUrl": "acme://items?item=42",
"slug": "spring",
"parameters": {
"item": "42"
},
"isDeferred": false,
"clickedAt": null
},
"attribution": null
}Route on slug and parameters, or on deeplinkUrl if your app already understands those URLs. deepLink is null when there is nothing to route. On the first launch, attribution says whether the install is attributed, with the link, the UTM source, medium and campaign and the tap time, or organic; on later launches it is null.
iOS in plain Swift
import UIKit
enum Appy {
static let publishableKey = "appy_pk_..."
private static let endpoint = URL(string: "https://api.appy.to/v1/sdk/open")!
private static let defaults = UserDefaults.standard
static var installId: String {
if let id = defaults.string(forKey: "appy.installId") { return id }
let id = UUID().uuidString.lowercased()
defaults.set(id, forKey: "appy.installId")
return id
}
static func open(url: URL? = nil, completion: @escaping ([String: Any]?) -> Void) {
let firstLaunch = !defaults.bool(forKey: "appy.firstOpenDone")
var body: [String: Any] = [
"installId": installId,
"firstLaunch": firstLaunch,
"device": ["platform": "ios", "osVersion": UIDevice.current.systemVersion]
]
if let url { body["url"] = url.absoluteString }
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("Bearer \(publishableKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, _ in
let ok = (response as? HTTPURLResponse).map { (200..<300).contains($0.statusCode) } ?? false
let json = data.flatMap { try? JSONSerialization.jsonObject(with: $0) as? [String: Any] }
if ok && firstLaunch { defaults.set(true, forKey: "appy.firstOpenDone") }
DispatchQueue.main.async { completion(ok ? json?["deepLink"] as? [String: Any] : nil) }
}.resume()
}
}Call Appy.open { link in … } once at launch, and Appy.open(url: url) { link in … } from scene(_:continue:) or onContinueUserActivity for Universal Links on your link domain. You still need the Associated Domains entitlement applinks:<subdomain>.appy.to.
Android in plain Kotlin
Add the Play Install Referrer library (about 20 KB):
implementation("com.android.installreferrer:installreferrer:2.2")import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
object Appy {
private const val PUBLISHABLE_KEY = "appy_pk_..."
fun open(context: Context, url: String?, onLink: (JSONObject?) -> Unit) {
val prefs = context.getSharedPreferences("appy", Context.MODE_PRIVATE)
val installId = prefs.getString("installId", null)
?: UUID.randomUUID().toString().also { prefs.edit().putString("installId", it).apply() }
val firstLaunch = !prefs.getBoolean("firstOpenDone", false)
if (!firstLaunch || url != null) {
send(prefs, installId, firstLaunch, url, null, onLink)
return
}
val sent = AtomicBoolean(false)
val client = InstallReferrerClient.newBuilder(context).build()
client.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(code: Int) {
val referrer = if (code == InstallReferrerClient.InstallReferrerResponse.OK) {
runCatching { client.installReferrer.installReferrer }.getOrNull()
} else null
client.endConnection()
if (sent.compareAndSet(false, true)) send(prefs, installId, true, null, referrer, onLink)
}
override fun onInstallReferrerServiceDisconnected() {
if (sent.compareAndSet(false, true)) send(prefs, installId, true, null, null, onLink)
}
})
}
private fun send(prefs: SharedPreferences, installId: String, firstLaunch: Boolean, url: String?,
referrer: String?, onLink: (JSONObject?) -> Unit) = thread {
val body = JSONObject()
.put("installId", installId)
.put("firstLaunch", firstLaunch)
.put("device", JSONObject().put("platform", "android").put("osVersion", Build.VERSION.RELEASE))
url?.let { body.put("url", it) }
referrer?.let { body.put("installReferrer", it).put("installReferrerSource", "google_play") }
val connection = URL("https://api.appy.to/v1/sdk/open").openConnection() as HttpURLConnection
val link = runCatching {
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Authorization", "Bearer $PUBLISHABLE_KEY")
connection.setRequestProperty("Content-Type", "application/json")
connection.outputStream.use { it.write(body.toString().toByteArray()) }
if (connection.responseCode !in 200..299) return@runCatching null
if (firstLaunch) prefs.edit().putBoolean("firstOpenDone", true).apply()
JSONObject(connection.inputStream.bufferedReader().readText()).optJSONObject("deepLink")
}.getOrNull()
connection.disconnect()
Handler(Looper.getMainLooper()).post { onLink(link) }
}
}Call Appy.open(context, null) { link -> … } in your launcher activity and Appy.open(context, intent.data?.toString()) { link -> … } when an App Link on your link domain opens the app. Exclude the appy preferences from Auto Backup, or a restored phone skips its first launch.
React Native, Flutter and Unity
The recipe is the same in every runtime:
- Storage for
installId, the first-open flag and pending events: AsyncStorage,shared_preferences,PlayerPrefsor similar. - Incoming links from the framework’s deep link API (
Linkingin React Native, an app links plugin in Flutter,Application.deepLinkActivatedin Unity), sent asurl. - The Play Install Referrer on Android through a plugin that exposes it, or a small platform channel that calls
com.android.installreferrer:installreferrerlike the Kotlin example. - Two HTTP calls, as in the curl examples.
A React Native first launch, for example:
const res = await fetch('https://api.appy.to/v1/sdk/open', {
method: 'POST',
headers: {Authorization: `Bearer ${PUBLISHABLE_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
installId,
firstLaunch,
url: (await Linking.getInitialURL()) ?? undefined,
installReferrer: Platform.OS === 'android' ? referrer : undefined,
device: {platform: Platform.OS, osVersion: String(Platform.Version)},
}),
});
if (res.ok && firstLaunch) await AsyncStorage.setItem('appy.firstOpenDone', '1');
const {deepLink} = res.ok ? await res.json() : {deepLink: null};Web views and hybrid apps
When most of the app runs in a web view, make the calls from the native shell or from the page’s JavaScript with fetch. Keep installId and the first-open flag in storage that lasts as long as the app, not in session storage. Pass links that open the app from the native side to POST /v1/sdk/open as url, and read the install referrer natively on Android.
Events
curl https://api.appy.to/v1/sdk/events \
-H "Authorization: Bearer $APPY_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"installId": "7f0c2b9e-3a61-4c2e-9d0b-5c1e8f2a9b10",
"events": [
{"id": "2000000123456789", "name": "purchase",
"timestamp": "2026-09-25T10:06:00Z", "revenue": 19.98, "currency": "USD",
"properties": {"sku": "A1"}}
]
}'{
"accepted": 1,
"duplicates": 0,
"rejected": []
}The answer (202) says how many events were stored, how many were duplicates of earlier ones, and which were rejected with a reason. Rejected events will never succeed; drop them.
An event id is 1 to 128 printable ASCII characters without spaces. It is the deduplication key for the app, shared with server events: use a UUID for ordinary events, and the App Store transaction id or Google Play order id for a purchase that your backend may report too.
From your server
Some events only your backend knows: a renewal charged by the store, a refund, an order that passed fraud checks. Send them with a secret key and either the installId or your own userId, the same one the app sent. Before you pay a referral bonus, look up how the user arrived and require attribution.verified. Both calls are described in the REST API guide.