Skip to content
aaxo.nz

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.

Rate limits

PlanRequests per minute, per key
Free and ProNo API access
business300
enterprise1,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

  1. Create a smart app link in your dashboard with your app link (e.g. myapp://product/42) and store links.
  2. 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.
  3. On first launch, your app calls POST /deferred/match with an app key and a random install ID it saves.
  4. AXO answers with the link, its app link and campaign tags, and records the install. Open deep_link to 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

MethodPathKeyWhat it does
POST/deferred/matchApp or secretCall once on first launch. Returns the smart link that led to the install and records the install.
POST/eventsApp or secretReport an in-app event (sign_up, purchase…), optionally with revenue. Tied to the install's link when known.
GET/eventsSecretList in-app events, newest first.
GET/installsSecretList installs with how they were matched (referrer, probabilistic or organic).
GET/linksSecretList your links. Supports limit, before and status.
POST/linksSecretCreate a short link or smart app link (validated like the dashboard).
GET/links/:id_or_slugSecretGet one link.
PATCH/links/:id_or_slugSecretChange destination, title or status (active or paused). Status archived moves the link to your archive and frees a slot.
GET/links/:id_or_slug/clicksSecretRecent clicks within your plan's history window.
GET/webhooksSecretList webhook subscriptions.
POST/webhooksSecretSubscribe an HTTPS address to events. Returns the signing secret once.
DELETE/webhooks/:idSecretUnsubscribe.
GET/meApp or secretCheck 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.

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).