Developers
Deep links, installs and data — through one small API
Open the right screen after an app install, see which links bring installs, send in-app events, manage links from your code, and get signed webhooks when anything happens.
New to the API? Start with the 5-minute quick start. Use of the API is covered by the Developer API terms.
Deferred deep linking
Someone taps your smart app link, installs from the store, and lands on the right screen on first open.
Install attribution
Every first launch is tied to the link that drove it — exact on Android, estimated on iPhone — or marked organic.
API & webhooks
Create and update links, pull clicks, installs and events, or subscribe to push them to your server.
Keys and authentication
Create keys on the Apps & API page (up to 10). Send them as Authorization: Bearer <key>. Base address: https://axo.nz/api/public/v1. Keys are shown once and stored only as a hash.
- Secret key (axo_sk_…) — for your servers. Full access to your account's links, clicks, installs, events and webhooks. Never put it in an app.
- App key (axo_pk_…) — safe to ship inside your iOS or Android app. It can only call
POST /deferred/matchandPOST /events.
Rate limits
| Plan | Requests per minute, per key |
|---|---|
| Free and Pro | No API access |
| business | 300 |
| enterprise | 1,200 |
Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). Over the limit you get HTTP 429 with Retry-After. Limits reset each calendar minute and short bursts can slightly exceed them. App keys share the same per-key limit, so a busy app may need its own key per platform.
Deferred deep linking and install attribution
- Create a smart app link in your dashboard with your app link (e.g.
myapp://product/42) and store links. - When someone without the app taps it on a phone, AXO sends them to the store. For Google Play we add an install referrer so the app can identify the exact click.
- On first launch, your app calls
POST /deferred/matchwith an app key and a random install ID it saves. - AXO answers with the link, its app link and campaign tags, and records the install. Open
deep_linkto take the user straight to the right screen.
How matching works. Android with Google Play: exact, via the install referrer (match_type: "referrer"). iPhone, Huawei AppGallery and Galaxy Store installs: estimated — the same network address, phone type and OS version within 2 hours ("probabilistic"). It can miss, or occasionally pick the wrong click when several people on one network tap your links at once. Network addresses are stored only as a one-way hash, used only for your own app, and deleted after 7 days. No match means "organic". Calling again for the same install returns the same answer with first_launch: false.
Android (Kotlin)
// build.gradle: implementation("com.android.installreferrer:installreferrer:2.2")
val referrerClient = InstallReferrerClient.newBuilder(context).build()
referrerClient.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(code: Int) {
val referrer = if (code == InstallReferrerClient.InstallReferrerResponse.OK)
referrerClient.installReferrer.installReferrer else null
referrerClient.endConnection()
// POST https://axo.nz/api/public/v1/deferred/match with your app key:
// { "install_id": <random UUID saved on first launch>, "platform": "android",
// "os_version": Build.VERSION.RELEASE, "app_id": packageName, "install_referrer": referrer }
// If the response has "matched": true, open response.deep_link.
}
override fun onInstallReferrerServiceDisconnected() {}
})iOS (Swift)
// On first launch only (store a flag and a random install_id in UserDefaults)
var req = URLRequest(url: URL(string: "https://axo.nz/api/public/v1/deferred/match")!)
req.httpMethod = "POST"
req.setValue("Bearer axo_pk_…", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: [
"install_id": installId, "platform": "ios",
"os_version": UIDevice.current.systemVersion,
"app_id": Bundle.main.bundleIdentifier ?? ""
])
URLSession.shared.dataTask(with: req) { data, _, _ in
// Decode JSON; if matched == true, route the user to deep_link
}.resume()Response
{
"matched": true,
"match_type": "referrer", // "referrer" | "probabilistic" | "organic"
"first_launch": true,
"install_id": "5f0c…",
"link": { "id": "…", "slug": "spring-sale", "title": "Spring sale" },
"deep_link": "myapp://product/42",
"destination": "https://example.com/p/42?utm_source=instagram",
"params": { "utm_source": "instagram" }
}In-app events
Send events like sign_up or purchase with the same install ID. Events from attributed installs count towards the link that brought them, and revenue is summed per currency on the Apps & API page.
curl -X POST https://axo.nz/api/public/v1/events \
-H "Authorization: Bearer axo_pk_…" -H "Content-Type: application/json" \
-d '{"name":"purchase","install_id":"5f0c…","revenue":19.99,"currency":"USD","properties":{"plan":"annual"}}'Endpoints
| Method | Path | Key | What it does |
|---|---|---|---|
| POST | /deferred/match | App or secret | Call once on first launch. Returns the smart link that led to the install and records the install. |
| POST | /events | App or secret | Report an in-app event (sign_up, purchase…), optionally with revenue. Tied to the install's link when known. |
| GET | /events | Secret | List in-app events, newest first. |
| GET | /installs | Secret | List installs with how they were matched (referrer, probabilistic or organic). |
| GET | /links | Secret | List your links. Supports limit, before and status. |
| POST | /links | Secret | Create a short link or smart app link (validated like the dashboard). |
| GET | /links/:id_or_slug | Secret | Get one link. |
| PATCH | /links/:id_or_slug | Secret | Change destination, title or status (active or paused). Status archived moves the link to your archive and frees a slot. |
| GET | /links/:id_or_slug/clicks | Secret | Recent clicks within your plan's history window. |
| GET | /webhooks | Secret | List webhook subscriptions. |
| POST | /webhooks | Secret | Subscribe an HTTPS address to events. Returns the signing secret once. |
| DELETE | /webhooks/:id | Secret | Unsubscribe. |
| GET | /me | App or secret | Check a key: plan and rate limit. |
Lists return { data, next_before }; pass next_before as ?before= for the next page. Bodies are JSON, up to 16 KB.
Webhooks
Subscribe an HTTPS address from the Apps & API page or with POST /webhooks. Up to 10 webhooks; each gets up to 120 deliveries a minute (extra events that minute are skipped). Reply with any 2xx status within 8 seconds. Deliveries are not retried; after 25 failures in a row the webhook is switched off. Pull endpoints let you catch up on anything missed.
link.clicked— Link clickedapp.install— App install / first openapp.event— In-app eventlink.created— Link createdlink.updated— Link changed
POST https://your-server.com/axo-webhook
AXO-Event: app.install
AXO-Signature: t=1767225600,v1=5d41402abc4b2a76b9719d911017c592…
{ "id": "evt-uuid", "type": "app.install", "created_at": "2026-10-27T07:00:00.000Z",
"data": { "install_id": "5f0c…", "platform": "android", "match_type": "referrer",
"link": { "id": "…", "slug": "spring-sale", "title": "Spring sale" }, "params": {} } }Verify the signature (Node.js)
import crypto from "node:crypto";
// rawBody: the exact request body string; header: the AXO-Signature header
export function verifyAxo(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // replay guard
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return expected.length === parts.v1?.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Errors
Errors return { "error": { "code", "message" } } with status 400 (invalid_request), 401 (unauthorized), 403 (wrong_key_type, plan_required, account_suspended), 404 (not_found), 409 (slug_taken), 422 (not_allowed, limit_reached) or 429 (rate_limited).