---
title: "Verify Android-Kotlin SDK events"
description: "Trace an Android-Kotlin SDK event through each stage: what the SDK captured, how plugins transformed it, what it sent, and what Amplitude received."
product: analytics
lang: en
last_updated: 2026-11-04
token_estimate: 2711
---
# Verify Android-Kotlin SDK events

An event moves through four stages between your `track()` call and Amplitude's servers. Verify each stage in order. The first stage where the event looks wrong is where the problem is.

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

| Stage | Question | Where to look |
| --- | --- | --- |
| [Captured](#check-what-the-sdk-captured) | Did the SDK accept the call? | Debug logs in Logcat |
| [Transformed](#check-how-plugins-transformed-the-event) | What did plugins add, change, or drop? | An inspection plugin |
| [Sent](#check-what-the-sdk-sent) | Did the request leave the device, and with what payload? | Debug logs, a proxy, or a custom HTTP client |
| [Received](#check-what-amplitude-received) | Did Amplitude accept and store the event? | The event callback, then **User Lookup** in Amplitude |

For problems common to every Amplitude SDK, go to [SDK troubleshooting and debugging](https://amplitude.com/docs/sdks/sdk-debugging). If behavior changed after an SDK upgrade, check the [Android-Kotlin SDK release notes](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk-release-notes).

## Turn on debug logging

The SDK logs to Logcat with the tag `Amplitude`. The default level is `INFO`. Set it to `DEBUG` while you verify:

#### Kotlin

```kotlin
import com.amplitude.common.Logger

amplitude.logger.logMode = Logger.LogMode.DEBUG
```

#### Java

```java
import com.amplitude.common.Logger;

amplitude.getLogger().setLogMode(Logger.LogMode.DEBUG);
```

The levels are `DEBUG`, `INFO`, `WARN`, `ERROR`, and `OFF`. In Android Studio, open **Logcat** and filter on `tag:Amplitude`. Return the level to `INFO` or higher before you ship.

### Send logs to your own logger

To route SDK logs somewhere other than Logcat, such as your crash reporter, implement `LoggerProvider` and set `loggerProvider`:

```kotlin
import com.amplitude.common.Logger
import com.amplitude.core.LoggerProvider

class MyLogger : Logger {
    override var logMode: Logger.LogMode = Logger.LogMode.WARN
    override fun debug(message: String) { /* ... */ }
    override fun info(message: String) { /* ... */ }
    override fun warn(message: String) { /* ... */ }
    override fun error(message: String) { /* ... */ }
}

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    loggerProvider = object : LoggerProvider {
        override fun getLogger(amplitude: com.amplitude.core.Amplitude): Logger = MyLogger()
    }
}
```

## Check what the SDK captured

With debug logging on, the SDK logs one line for each event it accepts:

```text
D/Amplitude: Logged event with type: Sign Up Clicked
```

If that line doesn't appear, look for one of these instead:

| Logcat message | Meaning |
| --- | --- |
| `Skip event for opt out config.` | Opt-out is on. The SDK drops every event. |
| `Event has an empty event type, dropping it.` | The event name is empty. |
| `Dropping event '<type>': this Amplitude instance was replaced.` | Your code created a newer instance with the same `instanceName`. Call `track()` on the newer instance. |
| `Invalid revenue object, missing required fields` | You passed a `Revenue` without `price`. |
| `Attempting to perform operation ... ignoring` (in standard output, not Logcat) | An `Identify` operation had an empty property name or a `null` value, or repeated a property. |

At this stage you can also read the state the SDK attaches to every event:

```kotlin
amplitude.isBuilt.await() // inside a coroutine
amplitude.getDeviceId() // "3f6c1e2a-...R"
amplitude.getUserId() // "user-12345" or null
amplitude.sessionId // 1761480000000, or -1 before the first session
amplitude.optOut // false
```

## Check how plugins transformed the event

After capture, each event passes through the plugin pipeline in a fixed order:

1. **Before plugins** (`Plugin.Type.Before`): The built-in context plugin adds `device_id`, `user_id`, `session_id`, `time`, `insert_id`, `library`, `platform`, device and OS fields, and `ip`, subject to [`trackingOptions`](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/reference#trackingoptions).
2. **Enrichment plugins** (`Plugin.Type.Enrichment`): Your own enrichment plugins run here, in the order you add them.
3. **Destination plugins** (`Plugin.Type.Destination`): The built-in Amplitude destination writes the event to the upload queue.

A before or enrichment plugin that returns `null` drops the event. To see the exact event the SDK sends, add a destination plugin that records each event:

```kotlin
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.DestinationPlugin

class InspectEvents : DestinationPlugin() {
    override fun track(payload: BaseEvent): BaseEvent {
        amplitude.logger.debug("Final event: ${payload.eventType} ${payload.eventProperties} device=${payload.deviceId} session=${payload.sessionId}")
        return payload
    }
}

val inspector = InspectEvents()
amplitude.add(inspector)
```

Destination plugins receive the event after every before and enrichment plugin has run. Identify events arrive in `identify()`, and revenue events in `revenue()`. Remove the plugin with `amplitude.remove(inspector)` when you're done.

## Check what the SDK sent

The SDK uploads events with a `POST` request to the endpoint for your configuration:

| Configuration | Request URL |
| --- | --- |
| Default (US) | `https://api2.amplitude.com/2/httpapi` |
| `ServerZone.EU` | `https://api.eu.amplitude.com/2/httpapi` |
| `useBatch = true` (US) | `https://api2.amplitude.com/batch` |
| `useBatch = true` (EU) | `https://api.eu.amplitude.com/batch` |
| `serverUrl` set | The URL you set |

The SDK uploads when 30 events (`flushQueueSize`) are waiting, 30 seconds (`flushIntervalMillis`) after an event is queued, when the app goes to the background, and when the device reconnects. To send right away, call `amplitude.flush()`.

The request body has this shape:

```json
{
  "api_key": "YOUR_API_KEY",
  "client_upload_time": "2026-10-26T12:00:12.400Z",
  "events": [
    {
      "event_type": "Sign Up Clicked",
      "event_properties": { "plan": "starter" },
      "device_id": "3f6c1e2a-...R",
      "session_id": 1761480000000,
      "time": 1761480012345,
      "insert_id": "9c0e...",
      "platform": "Android",
      "os_name": "android",
      "ip": "$remote",
      "library": "amplitude-analytics-android/1.37.0"
    }
  ]
}
```

`"ip": "$remote"` tells Amplitude to use the request's IP address. To omit it, call `trackingOptions.disableIpAddress()`.

> **Note:** Compressed request bodies
>
> For Amplitude's own endpoints, the SDK gzip-compresses every request body and sends `Content-Encoding: gzip`. For a custom `serverUrl`, it sends plain JSON unless you set `enableRequestBodyCompression = true`. Decompress the body before you read it in a proxy.

The debug log doesn't print request bodies. To inspect request bodies, use one of these methods:

- **A local proxy**: Set `serverUrl` to a proxy you control, such as one running on your development machine, and read the body there. The proxy receives plain JSON unless you turn on compression.
- **A custom HTTP client**: Set `httpClient` to an `HttpClientInterface` that logs the `events` string before it uploads. Go to [Use a custom HTTP client](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/instrument-and-configure#use-a-custom-http-client).

If no request appears:

| Cause | How to confirm | Fix |
| --- | --- | --- |
| The device is offline | The SDK holds uploads while `offline` is `true`. It flushes when the device reconnects. | Expected. Reconnect, or call `flush()` after the device is online. |
| You set `offline = true` | Your code or configuration sets `offline`. | Set `offline = false`. |
| The upload interval hasn't passed | Fewer than 30 events are queued, and less than 30 seconds has passed. | Call `amplitude.flush()`. |
| The network blocks Amplitude | Logcat shows `Request failed: ...`. | Route events through a [proxy](https://amplitude.com/docs/sdks/analytics/android/android-kotlin-sdk/instrument-and-configure#send-events-through-a-proxy). |

## Check what Amplitude received

### Read the upload result

Pass a callback to `track()` to get the result for one event, or set `callback` in the configuration to get every result:

```kotlin
import com.amplitude.core.events.BaseEvent

amplitude.track(BaseEvent().apply { eventType = "Sign Up Clicked" }) { event, status, message ->
    Log.d("MyApp", "${event.eventType}: $status $message")
}
```

The SDK calls the callback only when it finishes with an event, either because Amplitude accepted it or because the SDK dropped it. For errors that the SDK retries, it keeps the event and doesn't call the callback until a later attempt finishes.

| Server response | `status` and `message` | What the SDK does |
| --- | --- | --- |
| `200` | `200`, `Event sent success.` | Removes the event from the queue. |
| `400`, invalid API key | `400`, the server error, for example `Invalid API key: ...` | Drops every event in the request and logs `Invalid API key. Don't retry.` |
| `400`, some events invalid or silenced | `400`, the server error, for the dropped events | Drops the events the server marked, and uploads the rest again. |
| `400` with no event details, for example from a proxy | No callback | Retries with backoff. |
| `413` | `413`, the server error, only for a single event that's still too large | Splits the request in half and retries. Drops a single event that's still too large. |
| `429` | No callback | Waits 30 seconds, then retries. |
| `408`, `5xx`, and any other status | No callback | Retries with backoff: about 1, 8, 32, and 128 seconds, then every 5 minutes, with no limit. |

A successful response body looks like this:

```json
{
  "code": 200,
  "events_ingested": 1,
  "payload_size_bytes": 412,
  "server_upload_time": 1761480012612
}
```

A `200` response means Amplitude's ingestion API accepted the event. Amplitude can still exclude an accepted event from analysis later, for example when a [block filter](https://amplitude.com/docs/data/remove-invalid-data) matches it.

### Find the event in Amplitude

1. Open **User Lookup** and search for the event's `user_id`, or its `device_id` if the user is anonymous.
2. Select the **Activity** tab and turn on **Live event updates**.
3. Open the event and compare its properties with what your inspection plugin logged.

For events from many users, use the [Ingestion Debugger](https://amplitude.com/docs/analytics/debug-analytics#ingestion-debugger) to review accepted and rejected requests for the project. Charts can take a few minutes longer to include new events than User Lookup does.

