> 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/developer-guide/handling-callbacks.md).

# Handling callbacks

The SDK emits internal lifecycle events — "I initialized," "a session started," "a campaign was shown" — that your app can subscribe to. Most apps ignore them. A few use them for diagnostics, to gate UI until the SDK is ready, or to log campaign impressions into their own analytics tool.

A PM should read this as: "the developer can optionally listen for SDK milestones. Useful for debugging or mirroring campaign impressions into another system, not required for the SDK to work."

**Use this when** you want to react to SDK lifecycle — log initialization timing, block UI until first config arrives, mirror campaign impressions. **Don't use this when** you just want to track app-level actions — that's [Tracking events](/developer-guide/tracking-events.md).

## What gets emitted

The SDK emits system events that your listener can observe: `SdkInitialized`, `ConfigFetchStarted`, `ConfigFetchFinished`, `SessionStarted`, `SessionFinished`, `CampaignShown`, `EventTriggered`, and `CustomPropertyChanged`. `CampaignResolved` is stats-only and is not delivered to this listener. See [Events](/reference/events.md) for the full canonical list, each event's exact properties, and which ones are observable here.

Each event carries a `name`, a `timestamp`, a `type` (`system`), and a `properties` map with event-specific details.

## Subscribing to system events

{% tabs %}
{% tab title="iOS (Swift)" %}

```swift
import AmplySDK

class SdkEventsAdapter: SystemEventsListener {
    func onEvent(event: EventInterface) {
        print("Amply system event: \(event.name) props: \(event.properties)")
        // Example: log to your analytics tool
        if event.name == "CampaignShown" {
            Analytics.log("amply_campaign_shown", params: event.properties)
        }
    }
}

// Hold a strong reference from your own side (e.g., on AppDelegate) so the adapter stays alive,
// and hold the token with it - the token is what detaches the listener later.
let adapter = SdkEventsAdapter()
let systemEventsToken = amply.setSystemEventsListener(listener: adapter)
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
import tools.amply.sdk.events.EventInterface
import tools.amply.sdk.events.SystemEventsListener

class SdkEventsAdapter : SystemEventsListener {
    override fun onEvent(event: EventInterface) {
        Log.d("Amply", "system event: ${event.name} props=${event.properties}")
        if (event.name == "CampaignShown") {
            Analytics.log("amply_campaign_shown", event.properties)
        }
    }
}

// Keep both: the adapter, and the token that detaches it.
val adapter = SdkEventsAdapter()
val systemEventsToken = amply.setSystemEventsListener(adapter)
```

{% endtab %}

{% tab title="React Native (TS)" %}

```ts
import Amply, { systemEvents, formatSystemEventLabel } from '@amplytools/react-native-amply-sdk';
import { useEffect } from 'react';

export function useSdkDiagnostics() {
  useEffect(() => {
    let unsubscribe: (() => void) | undefined;
    let unmounted = false;

    systemEvents.addListener(event => {
      console.log('Amply system event:', formatSystemEventLabel(event));
      if (event.name === 'CampaignShown') {
        Analytics.log('amply_campaign_shown', event.properties);
      }
    })
      .then(unsub => {
        if (unmounted) unsub();
        else unsubscribe = unsub;
      });

    return () => {
      unmounted = true;
      unsubscribe?.();
    };
  }, []);
}
```

{% endtab %}
{% endtabs %}

## React Native hook

React Native also exposes a hook that collects recent system events for rendering in a debug view:

```tsx
import { useAmplySystemEvents, formatSystemEventLabel } from '@amplytools/react-native-amply-sdk';

export function SdkEventsLog() {
  const { events, reset } = useAmplySystemEvents({ maxEntries: 50 });

  return (
    <View>
      <Button title="Clear" onPress={reset} />
      {events.map(e => (
        <Text key={`${e.name}-${e.timestamp}`}>
          {formatSystemEventLabel(e)}
        </Text>
      ))}
    </View>
  );
}
```

`useAmplySystemEvents` accepts `{ maxEntries, dedupe, onEvent }`. It manages subscription and cleanup for you.

## Pending events at startup

System events can fire before your listener is registered (for example, `SdkInitialized` fires during construction). The SDK queues these early events and delivers them to the listener as soon as it's set. Register your listener as early as possible — ideally immediately after SDK construction — to see the full timeline.

## When to use this API vs ignore it

Use it:

* Mirroring `CampaignShown` into your own analytics tool to reconcile funnels
* Blocking a loading state until `ConfigFetchFinished` on first launch
* Logging `SessionStarted`/`SessionFinished` timing for diagnostics

Ignore it:

* For normal event tracking — call `track(...)` directly instead
* For deeplink handling — use `registerDeepLinkListener` / `addDeepLinkListener`
* For remote config — the SDK applies it internally

## One listener at a time (native iOS / Android)

On iOS and Android, `setSystemEventsListener` returns a token - pass it to `clearSystemEventsListener(token)` to detach - and replaces any previously-set listener. On React Native, `systemEvents.addListener` adds to a list of subscribers and returns an unsubscribe function per subscription.

## Withdraw the listener when the object registering it goes away

Requires apps built with SDK 0.9.0 or later. On native iOS and Android, registering returns a token: call `clearSystemEventsListener(token)` — and `removeDeepLinkListener(token)` for deep links — when the object you registered is being torn down.

Most apps register once, from the application object, and never need this. It matters when you register from something with a shorter life than the SDK, because the SDK keeps running and keeps calling what it was given. Deep link listeners make this visible fastest: registering **adds** one rather than replacing it, so a component that registers each time it is rebuilt leaves the previous listeners behind and every later deep link is delivered to all of them.

Withdrawing takes the **token**, not the listener, so keep the token — that is the only thing that can detach it. A listener written inline is fine, because what you hold on to is the token rather than the object. Withdrawing with a token that is no longer the current registration does nothing, which is deliberate: it stops a component that is going away from silencing the one that already replaced it. Detach from your own teardown hook rather than the object's deallocation callback, which will not run while the SDK still holds a reference.

`clearSystemEventsListener` clears only if your listener is still the one installed, so it cannot silence a replacement that registered after you. `removeDeepLinkListener` removes the listener you pass — one registration per call, so a listener registered twice must be withdrawn twice.

On React Native you do not call these. The JavaScript `addDeepLinkListener` and `systemEvents.addListener` already give you an unsubscribe function for your own subscriptions.

## Related

* [Tracking events](/developer-guide/tracking-events.md) — your custom events, not SDK lifecycle
* [Testing your integration](/developer-guide/testing-your-integration.md) — use these callbacks to confirm initialization and config fetch
* [iOS SDK reference](/reference/sdk-ios.md) — exact signatures
* [Android SDK reference](/reference/sdk-android.md) — exact signatures
* [React Native SDK reference](/reference/sdk-react-native.md) — exact signatures
