> For the complete documentation index, see [llms.txt](https://docs.amply.tools/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amply.tools/reference/sdk-android.md).

# Android SDK

Public API surface for the Amply Android SDK. Kotlin signatures are copied verbatim from the source.

## At a glance

| Group               | Function                                                                                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Initialization      | `Amply(config, application)`, `AmplyConfig(...)`, `amplyConfig { ... }`                                                                                                   |
| Event tracking      | `track(event, properties)`                                                                                                                                                |
| App language        | `setAppLanguage(tag)`, `AmplyConfig.appLanguage`                                                                                                                          |
| User attributes     | `setUserId(userId)`, `setCustomProperty(key, value)`, `setCustomProperties(properties)`, `getCustomProperty(key)`, `removeCustomProperty(key)`, `clearCustomProperties()` |
| Deeplinks           | `registerDeepLinkListener(listener)` → token, `removeDeepLinkListener(token)`, `handleUrl(url)` (0.15.0+)                                                                 |
| System events       | `setSystemEventsListener(listener)` → token, `clearSystemEventsListener(token)`                                                                                           |
| Last campaign touch | `getLastCampaignTouch(withinMs)` (0.15.0+)                                                                                                                                |
| Data inspection     | `getRecentEvents(limit)`, `getDataSetSnapshot(type)`                                                                                                                      |
| Logging             | `setLogLevel(level)`, `getLogLevel()`, `setLogListener(listener)`                                                                                                         |

## Detaching a listener

Registering a listener returns a **token**. Keep it — passing that token back is how you detach:

```
val token = amply.registerDeepLinkListener(router)
// later, when whatever owns `router` goes away:
amply.removeDeepLinkListener(token)
```

Detaching with a token that is no longer the current registration does nothing. That is deliberate: if a screen is replaced by a new one that registers first, the departing screen cannot silence its replacement on the way out.

**Who needs to do this.** A listener set up once at launch and kept for the life of the app never has to detach. Anything shorter-lived does — a screen, a feature module. A listener left attached after its owner is gone keeps that owner alive and the SDK keeps calling it.

**Detach where your teardown actually runs.** Do not rely on the object's deallocation hook: while the SDK still holds the listener, that hook does not run. Use whatever explicit teardown point your host gives you.

**Gates work the same way.** `registerGate` returns a token and `unregisterGate` takes it. A gate left registered by a screen that has gone away makes the next gated call on that URL wait out its full timeout with nothing on screen.

## Initialization

### `class Amply(val config: AmplyConfig, val application: Application)`

Construct the SDK. Construction **is** initialization — the `init` block wires internal components, starts the first session, and begins tracking the activity lifecycle. No separate `initialize()` call is required.

Parameters:

| Name          | Type                      | Required | Description                                                              |
| ------------- | ------------------------- | -------- | ------------------------------------------------------------------------ |
| `config`      | `AmplyConfig`             | yes      | Credentials and optional network overrides.                              |
| `application` | `android.app.Application` | yes      | Application instance used for lifecycle callbacks, storage, and context. |

Hold the returned instance for the lifetime of the process (commonly on the `Application` subclass).

### `class AmplyConfig`

Credential and network configuration.

```kotlin
class AmplyConfig(
    val appId: String,
    val apiKeyPublic: String,
    val apiKeySecret: String,
    val defaultConfig: String?,
    val configBaseUrl: String? = null,
    val backendBaseUrl: String? = null,
)
```

| Name             | Type      | Required       | Description                                                                                      |
| ---------------- | --------- | -------------- | ------------------------------------------------------------------------------------------------ |
| `appId`          | `String`  | yes            | Application identifier from the Amply dashboard.                                                 |
| `apiKeyPublic`   | `String`  | yes            | Public API key.                                                                                  |
| `apiKeySecret`   | `String`  | yes            | Secret API key. Keep it out of source control.                                                   |
| `defaultConfig`  | `String?` | yes (nullable) | JSON string of a fallback configuration used until the remote config loads. Pass `null` to skip. |
| `configBaseUrl`  | `String?` | no             | Override for the configuration service base URL.                                                 |
| `backendBaseUrl` | `String?` | no             | Override for the events backend base URL.                                                        |

### `fun amplyConfig(initializer: AmplyConfigBuilder.() -> Unit): AmplyConfig`

Kotlin DSL for building an `AmplyConfig`. Enforces that `appId`, `apiKeyPublic`, and `apiKeySecret` are non-null and throws `IllegalArgumentException` otherwise.

```kotlin
val config = amplyConfig {
    api {
        appId = "your.app.id"
        apiKeyPublic = "your-public-key"
        apiKeySecret = "your-secret-key"
    }
    network {
        configBaseUrl = "https://config.example.com"
        backendBaseUrl = "https://events.example.com"
    }
    defaultConfig = "{ ... }"
}
```

The `api { }` block sets credentials; the `network { }` block sets optional overrides. Returns a fully validated `AmplyConfig`.

## Event tracking

### `fun track(event: String, properties: Map<String, Any> = emptyMap())`

Record a custom event.

| Name         | Type               | Required | Description                                 |
| ------------ | ------------------ | -------- | ------------------------------------------- |
| `event`      | `String`           | yes      | Event name.                                 |
| `properties` | `Map<String, Any>` | no       | Event properties. Defaults to `emptyMap()`. |

Returns `Unit`. Events are persisted locally and flushed by the SDK; no network call is tied to this call site.

## Gating

Gate a user-facing step on a campaign's outcome — for example, a rewarded ad before an export. See [Gating actions](/developer-guide/gating-actions.md) for the full flow.

### `suspend fun trackGated(event: String, properties: Map<String, Any> = emptyMap()): GateDecision`

Track an event and suspend until any gating campaign on it resolves, then return the decision. If no gate applies, returns `GateDecision.Proceed(ProceedReason.FailOpen)` immediately. Call from a coroutine.

| Name         | Type               | Required | Description                     |
| ------------ | ------------------ | -------- | ------------------------------- |
| `event`      | `String`           | yes      | Event name a campaign may gate. |
| `properties` | `Map<String, Any>` | no       | Event properties.               |

### `fun registerGate(baseUrl: String, presenter: CampaignPresenter, onAbort: AbortPolicy = AbortPolicy.Cancel, timeoutMs: Long = 60_000): ListenerToken`

Register the single gate presenter once at startup. The SDK calls the presenter when a gated campaign needs to show UI, and calls its `dismiss()` if the gate is abandoned.

| Name        | Type                | Required | Description                                              |
| ----------- | ------------------- | -------- | -------------------------------------------------------- |
| `baseUrl`   | `String`            | yes      | Base URL the gate resolves against.                      |
| `presenter` | `CampaignPresenter` | yes      | Your presenter (see below).                              |
| `onAbort`   | `AbortPolicy`       | no       | What a user dismiss means. Default `AbortPolicy.Cancel`. |
| `timeoutMs` | `Long`              | no       | Fail-open deadline in milliseconds. Default `60_000`.    |

### `fun unregisterGate(token: ListenerToken)`

Withdraws the gate registered with that token. The URL stays gate-able, so a gated call still resolves immediately rather than falling through as an ordinary deeplink - it resolves the way the gate was registered to resolve, and costs the campaign no impression.

A gate registered at launch and kept for the life of the app needs no withdrawal. One owned by something shorter-lived does: the registry outlives whatever registered into it.

### `sealed class GateDecision`

| Variant                                       | Meaning                                                                     |
| --------------------------------------------- | --------------------------------------------------------------------------- |
| `GateDecision.Proceed(reason: ProceedReason)` | Do the next step. `reason` distinguishes a satisfied gate from a fail-open. |
| `GateDecision.Cancelled`                      | User deliberately dismissed a `Cancel`-policy gate. Suppress the next step. |

`when (decision) { is GateDecision.Proceed -> … ; is GateDecision.Cancelled -> … }`.

### `enum class ProceedReason`

`ProceedReason.Completed` — the campaign's value-exchange was satisfied (e.g. reward earned). `ProceedReason.FailOpen` — nothing to wait on, or the gate couldn't run to a decision; do not reward.

### `enum class AbortPolicy`

`AbortPolicy.Cancel` (default) — a dismiss returns `GateDecision.Cancelled` (true gating, for value-exchange actions). `AbortPolicy.Proceed` — a dismiss fails open (for consent-style flows).

### `interface CampaignPresenter`

```kotlin
interface CampaignPresenter {
    fun present(params: Map<String, String>, info: Map<String, Any>, resolution: CampaignResolution)
    fun dismiss()
}
```

`present` shows the gated UI and reports the result through the `resolution` handle; `dismiss` tears it down without producing a result (the SDK calls it when the gate is abandoned). Both are required.

### `CampaignResolution` / `enum class CampaignResult`

The `resolution` handle passed to `present(...)` reports the outcome exactly once:

```kotlin
resolution.resolve(CampaignResult.Completed)    // value-exchange satisfied → proceeds (ProceedReason.Completed)
resolution.resolve(CampaignResult.Dismissed)    // user backed out → honors the onAbort policy
resolution.resolve(CampaignResult.Unavailable)  // couldn't run (no fill / error) → fail-open
```

`CampaignResult`: `Completed` / `Dismissed` / `Unavailable`.

## App language

### `fun setAppLanguage(tag: String?)`

Tell Amply which language your app shows. Sheets, stories and mini-apps use it to pick a translation, and the **App language** targeting rule matches on it. Pass `null` to go back to automatic detection (the per-app language from Android 13 settings or AppCompat, else the device language).

| Name  | Type      | Required | Description                                                                       |
| ----- | --------- | -------- | --------------------------------------------------------------------------------- |
| `tag` | `String?` | yes      | A language tag in any common form (`fr`, `pt-BR`, `pt_BR`, `zh-Hant`), or `null`. |

Returns `Unit`. Remembered across launches until changed. A value that is not a language tag is ignored with a warning in the log. To set it before the first campaign is evaluated, use `appLanguage` in `AmplyConfig`. See [App language](/developer-guide/app-language.md).

## User attributes

### `fun setUserId(userId: String?)`

Associate subsequent events with a user identifier.

| Name     | Type      | Required | Description                                 |
| -------- | --------- | -------- | ------------------------------------------- |
| `userId` | `String?` | yes      | Stable user identifier, or `null` to clear. |

Returns `Unit`. Survives across sessions until changed.

### `fun setCustomProperty(key: String, value: Any)`

Set one custom property on the current user.

| Name    | Type     | Required | Description                                                                     |
| ------- | -------- | -------- | ------------------------------------------------------------------------------- |
| `key`   | `String` | yes      | Property key.                                                                   |
| `value` | `Any`    | yes      | One of: `String`, `Int`, `Long`, `Float`, `Double`, `Boolean`, `DateTimeValue`. |

Returns `Unit`. Use `DateTimeValue(epochMillis)` to persist a value as a datetime rather than as a number.

### `fun setCustomProperties(properties: Map<String, Any>)`

Set multiple custom properties at once.

| Name         | Type               | Required | Description               |
| ------------ | ------------------ | -------- | ------------------------- |
| `properties` | `Map<String, Any>` | yes      | Keys to supported values. |

Returns `Unit`. Equivalent to calling `setCustomProperty` per entry.

### `suspend fun getCustomProperty(key: String): Any?`

Read the current value of a custom property.

| Name  | Type     | Required | Description   |
| ----- | -------- | -------- | ------------- |
| `key` | `String` | yes      | Property key. |

Returns `Any?` — the stored value, or `null` if unset. `suspend` because storage access runs off the main thread.

### `fun removeCustomProperty(key: String)`

Delete a single custom property.

| Name  | Type     | Required | Description             |
| ----- | -------- | -------- | ----------------------- |
| `key` | `String` | yes      | Property key to remove. |

Returns `Unit`.

### `fun clearCustomProperties()`

Remove every custom property on the current user. Returns `Unit`. Takes no parameters.

## Deeplinks

### `fun registerDeepLinkListener(listener: DeepLinkListener): ListenerToken`

Register a handler that receives campaign deep link URLs.

| Name       | Type               | Required | Description                                            |
| ---------- | ------------------ | -------- | ------------------------------------------------------ |
| `listener` | `DeepLinkListener` | yes      | Handler implementing `onDeepLink(url, info): Boolean`. |

Returns `Unit`. Multiple listeners can be registered; each is invoked in registration order.

### `interface DeepLinkListener`

```kotlin
interface DeepLinkListener {
    fun onDeepLink(url: String, info: Map<String, Any>): Boolean
}
```

Return `true` if your code handled the URL; `false` to allow other listeners to handle it.

A listener that throws is treated as "did not handle it" — the URL still reaches the listeners registered after it.

### `fun removeDeepLinkListener(token: ListenerToken)`

Withdrawing takes the **token** returned at registration, not the listener object. A token that is no longer the current registration is ignored, so a component being torn down cannot detach the one that replaced it.

Stop sending deep links to a previously registered listener.

| Name       | Type               | Required | Description               |
| ---------- | ------------------ | -------- | ------------------------- |
| `listener` | `DeepLinkListener` | yes      | The listener to withdraw. |

Returns `Unit`. Registering **adds** a listener rather than replacing one, so anything you register from an object that can be torn down and rebuilt must be withdrawn when that object goes away — otherwise the old listeners stay registered and every later deep link is delivered to all of them.

### `fun handleUrl(url: String): Boolean`

**SDK 0.15.0+.** Hand Amply a link your app was opened with. Call it for every incoming link, on a cold start and while running, before your own routing. Returns `true` for an Amply test link from [Test on device](/user-guide/testing-and-rollout.md#test-on-device), which Amply then shows and your app should not route; `false` for any other link, which is left untouched. See [Forwarding links to Amply](/developer-guide/handling-deeplinks.md#forwarding-links-to-amply).

| Name  | Type     | Required | Description            |
| ----- | -------- | -------- | ---------------------- |
| `url` | `String` | yes      | The full incoming URL. |

## System events

### `fun setSystemEventsListener(listener: SystemEventsListener): ListenerToken`

Receive SDK lifecycle events (session start, config fetch, campaign shown).

| Name       | Type                   | Required | Description                               |
| ---------- | ---------------------- | -------- | ----------------------------------------- |
| `listener` | `SystemEventsListener` | yes      | Handler with one `onEvent(event)` method. |

Returns `Unit`. Replaces any previously set listener.

### `interface SystemEventsListener`

```kotlin
interface SystemEventsListener {
    fun onEvent(event: EventInterface)
}
```

### `fun clearSystemEventsListener(token: ListenerToken)`

Withdrawing takes the **token** returned at registration, not the listener object. A token that is no longer the current registration is ignored, so a component being torn down cannot detach the one that replaced it.

Stop sending system events to a previously set listener.

| Name       | Type                   | Required | Description               |
| ---------- | ---------------------- | -------- | ------------------------- |
| `listener` | `SystemEventsListener` | yes      | The listener to withdraw. |

Returns `Unit`. The listener is cleared **only if it is still the one currently set** — so if a replacement has already registered itself, withdrawing the old one leaves the new one in place. Call it when the object you registered is being torn down.

## Last campaign touch

**SDK 0.15.0+.** For crediting a purchase — or any conversion — to the campaign that came before it, in your own analytics as well as in Amply. See [Purchases from campaigns in your analytics](/recipes/purchases-from-campaigns-in-analytics.md).

### `suspend fun getLastCampaignTouch(withinMs: Long): CampaignTouch?`

The campaign that most recently reached this user, if that was at most `withinMs` milliseconds ago.

| Name       | Type   | Required | Description                                               |
| ---------- | ------ | -------- | --------------------------------------------------------- |
| `withinMs` | `Long` | yes      | How far back to look, in milliseconds. Capped at 30 days. |

Returns `CampaignTouch?` — `null` when no campaign reached the user within the window, before the SDK is initialized, and while the SDK is not collecting data. Never throws.

* **The newest touch wins.** A touch is a campaign being **shown**, or a campaign being **engaged** with. Amply keeps one touch per campaign — the newer of the two — and returns the most recent across all campaigns.
* **Engaged** means the campaign's action reported it finished: a mini-app's flow ended, or your [gated-action](/developer-guide/gating-actions.md) handler reported completion. A mini-app that closes itself also counts, so engaged is not proof the user reached the last screen. Deep link and Rate & Review campaigns only produce **shown**.
* **The window is inclusive and capped at 30 days.** A larger value behaves as 30 days.
* **Cleared when the app switches to a different user.** Changing the user id from one value to another, or to none, forgets every stored touch, so a purchase is never credited to a campaign another user saw. Setting a user id for the first time keeps them.
* **Starts with SDK 0.15.0.** Touches are recorded from the first launch on 0.15.0; campaigns shown on an earlier version are not known to it.

```kotlin
val touch = amply.getLastCampaignTouch(withinMs = 30 * 60 * 1000L)
if (touch != null) {
    Log.d("Amply", "${touch.campaignId} ${touch.campaignName} ${touch.kind} ${touch.atMs}")
}
```

### `data class CampaignTouch`

```kotlin
// package tools.amply.sdk.campaigns
data class CampaignTouch(
    val campaignId: String,
    val campaignName: String,
    val kind: CampaignTouchKind,
    val atMs: Long,
)

enum class CampaignTouchKind { SHOWN, ENGAGED }
```

| Field          | Description                                                                                               |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| `campaignId`   | The campaign's identifier — the same value `CampaignShown` reports and `{@campaign.id}` puts into a link. |
| `campaignName` | The campaign's name as it was when the touch happened. Empty if the campaign had no name then.            |
| `kind`         | `CampaignTouchKind.SHOWN` or `CampaignTouchKind.ENGAGED`.                                                 |
| `atMs`         | When it happened, UTC epoch milliseconds.                                                                 |

## Data inspection

### `suspend fun getRecentEvents(limit: Int = 30): List<EventInterface>`

Fetch the most recent events tracked in the current install. Intended for in-app debug tools.

| Name    | Type  | Required | Description                               |
| ------- | ----- | -------- | ----------------------------------------- |
| `limit` | `Int` | no       | Maximum number of events. Defaults to 30. |

Returns `List<EventInterface>` newest-first.

### `suspend fun getDataSetSnapshot(type: DataSetType): Map<String, Any>`

Return a snapshot of one dataset the SDK uses for targeting.

| Name   | Type          | Required | Description       |
| ------ | ------------- | -------- | ----------------- |
| `type` | `DataSetType` | yes      | Dataset selector. |

Returns `Map<String, Any>`.

### `sealed class DataSetType`

| Variant                                                        | Value             | Contents                                                                          |
| -------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------- |
| `DataSetType.Device`                                           | `@device`         | Advertising ID, model, OS version, locale, timezone.                              |
| `DataSetType.User`                                             | `@user`           | User ID and identity attributes.                                                  |
| `DataSetType.Custom`                                           | `@custom`         | All custom properties you've set.                                                 |
| `DataSetType.Session`                                          | `@session`        | Current session metadata.                                                         |
| `DataSetType.TriggeredEvent(countStrategy, params, eventName)` | `@triggeredEvent` | Aggregated counts for a specific event. `CountStrategy` is `GLOBAL` or `SESSION`. |
| `DataSetType.Events(data)`                                     | `@events`         | Counts for a list of events. Each `Event(name, type, params)`.                    |

## Logging

### `fun setLogLevel(level: LogLevel)`

Set the SDK log level.

| Name    | Type       | Required | Description                                           |
| ------- | ---------- | -------- | ----------------------------------------------------- |
| `level` | `LogLevel` | yes      | `LogLevel.NONE`, `ERROR`, `WARN`, `INFO`, or `DEBUG`. |

Returns `Unit`. Defaults to `NONE`.

### `fun setLogLevel(level: String?)`

String overload. Accepts `"none"`, `"error"`, `"warn"`, `"info"`, `"debug"` (case-insensitive). Any other value — including `null` — maps to `NONE`. Returns `Unit`.

### `fun getLogLevel(): LogLevel`

Return the current log level. Takes no parameters.

### `fun setLogListener(listener: LogListener): ListenerToken`

No longer accepts `null`. Detach with `clearLogListener(token)`.

Install a handler that receives every SDK log entry above the current level. Pass `null` to remove the existing listener.

| Name       | Type           | Required | Description                                      |
| ---------- | -------------- | -------- | ------------------------------------------------ |
| `listener` | `LogListener?` | yes      | Handler with `onLog(entry)`, or `null` to clear. |

Returns `Unit`.

### `interface LogListener`

```kotlin
interface LogListener {
    fun onLog(entry: LogEntry)
}
```

### `data class LogEntry`

| Field       | Type                | Description                                                      |
| ----------- | ------------------- | ---------------------------------------------------------------- |
| `level`     | `LogLevel`          | Severity of the entry.                                           |
| `category`  | `String`            | SDK-assigned category (e.g. `"sdk"`, `"session"`, `"deeplink"`). |
| `message`   | `String`            | Human-readable message.                                          |
| `timestamp` | `Long`              | UTC epoch milliseconds.                                          |
| `details`   | `Map<String, Any>?` | Structured context, if provided.                                 |

## Types

### `enum class LogLevel`

| Value   | Int level |
| ------- | --------- |
| `NONE`  | 0         |
| `ERROR` | 1         |
| `WARN`  | 2         |
| `INFO`  | 3         |
| `DEBUG` | 4         |

Higher values include all lower-severity entries.

### `class DateTimeValue(val epochMillis: Long)`

Wrapper passed to `setCustomProperty` to mark a value as a datetime rather than as a number.

### `interface EventInterface`

| Field        | Type               | Description                               |
| ------------ | ------------------ | ----------------------------------------- |
| `name`       | `String`           | Event name.                               |
| `timestamp`  | `Long`             | UTC epoch milliseconds.                   |
| `properties` | `Map<String, Any>` | Event properties.                         |
| `type`       | `EventType`        | `EventType.CUSTOM` or `EventType.SYSTEM`. |

### System event names

Well-known values on `EventInterface.name` when `type == EventType.SYSTEM`:

| Constant                               | Value                   | When it fires                                                                                                                                                              |
| -------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SystemEvents.SDK_INITIALIZED`         | `SdkInitialized`        | SDK finished initialization.                                                                                                                                               |
| `SystemEvents.CONFIG_FETCH_STARTED`    | `ConfigFetchStarted`    | Remote configuration fetch began.                                                                                                                                          |
| `SystemEvents.CONFIG_FETCH_FINISHED`   | `ConfigFetchFinished`   | Remote configuration fetch completed.                                                                                                                                      |
| `SystemEvents.SESSION_START`           | `SessionStarted`        | Session started. `properties["type"]` is `"cold"` or `"warm"`.                                                                                                             |
| `SystemEvents.SESSION_END`             | `SessionFinished`       | Session ended.                                                                                                                                                             |
| `SystemEvents.CAMPAIGN_SHOWN`          | `CampaignShown`         | Campaign impression recorded. `properties` carry `source`, `campaignId` and `campaignName` (0.15.0+).                                                                      |
| `SystemEvents.EVENT_TRIGGERED`         | `EventTriggered`        | A custom event matched a campaign trigger.                                                                                                                                 |
| `SystemEvents.CUSTOM_PROPERTY_CHANGED` | `CustomPropertyChanged` | A custom property was set, updated, removed, or cleared. Usable as a campaign trigger.                                                                                     |
| `SystemEvents.CAMPAIGN_RESOLVED`       | `CampaignResolved`      | A presented campaign action resolved, or a chosen campaign was skipped (0.5.0+). Mainly for stats; `properties` carry `campaignId`, `campaignName` (0.15.0+) and `result`. |

## Related

* [iOS SDK reference](/reference/sdk-ios.md) — Swift surface on the other native platform.
* [React Native SDK reference](/reference/sdk-react-native.md) — TypeScript surface for RN apps.
