---
title: Verify iOS Swift SDK events
description: "Trace an iOS Swift SDK event through each stage: what the SDK captured, how plugins transformed it, what it uploaded, and what Amplitude received."
product: analytics
lang: en
last_updated: 2026-11-05
token_estimate: 3123
---
# Verify iOS Swift 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 the Xcode console, and the SDK's identity state |
| [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 local proxy, or a capture endpoint |
| [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 [iOS Swift SDK release notes](https://amplitude.com/docs/sdks/analytics/ios/ios-swift-sdk-release-notes).

## Turn on debug logging

Pass `logLevel: .debug` to `Configuration` while you verify. The SDK reads `logLevel` only when you create the instance, so setting `amplitude.configuration.logLevel` later has no effect.

```swift
let amplitude = Amplitude(configuration: Configuration(
    apiKey: "YOUR_API_KEY",
    logLevel: .debug
))
```

The levels are `.off`, `.error`, `.warn` (default), `.log`, and `.debug`. The uppercase names, such as `LogLevelEnum.DEBUG`, still compile but are deprecated. The lowercase names require 1.17.0 or later.

The default logger writes to the unified logging system with subsystem `Amplitude` and category `Logging`. Each message starts with its level: `Error: `, `Warn: `, `Log: `, or `Debug: `. In the Xcode console, filter on `subsystem:Amplitude`. Run the app from Xcode to read the messages. Outside a debugger session, tools such as Console.app can show the message text as `<private>`.

Return `logLevel` to `.warn` before you ship. To send SDK logs to your own logging system instead, go to [Set the log level](https://amplitude.com/docs/sdks/analytics/ios/ios-swift-sdk/instrument-and-configure#set-the-log-level).

## Check what the SDK captured

The SDK doesn't log individual `track()` calls. With debug logging on, a successful capture and upload includes lines like these:

```text
Debug: Installing NetworkConnectivityCheckerPlugin, offline feature should be supported.
Debug: Trimming queued events..
Log: Start flushing 1 events
Debug: Successfully completed request
Debug: No event files to upload
```

`Start flushing N events` counts the events queued since the last upload, so it confirms that the SDK accepted your call. If the line never appears after `flush()`, check these causes:

- **The call didn't reach the configured instance.** The call never ran, or it ran on a different `Amplitude` instance from the one you configured. Each instance has its own queue and settings.
- **The instance was released.** An instance stored in a local variable stops uploading when Swift frees it. Keep a strong reference for the app's lifetime.
- **Opt-out is on.** The SDK logs `Log: Skip event based on opt out configuration` and drops the event before it reaches the queue.
- **The SDK is offline.** `flush()` logs `Debug: Skipping flush while offline.` and keeps the events. Go to [Use offline mode](https://amplitude.com/docs/sdks/analytics/ios/ios-swift-sdk/instrument-and-configure#use-offline-mode).

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

```swift
amplitude.getDeviceId()  // "EAB4A5BE-2294-4202-B9A8-EC96F02BA9D5"
amplitude.getUserId()    // "user-12345", or nil
amplitude.getSessionId() // 1793655975244
amplitude.optOut         // false
```

On iOS, tvOS, and visionOS, the SDK uses the identifier for vendor (IDFV) as the default device ID, so `device_id` and `idfv` match in the payload. On macOS, it uses the Mac's network hardware address.

## Check how plugins transformed the event

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

1. **Before plugins** (`.before`): the built-in context plugin adds `device_id`, `user_id`, `session_id`, `time`, `event_id`, `insert_id`, `library`, `platform`, `os_name`, `os_version`, `device_model`, `language`, `ip`, and the other device fields.
2. **Enrichment plugins** (`.enrichment`): your own plugins run here, in the order you add them.
3. **Destination plugins** (`.destination`): the built-in Amplitude destination queues the event for upload.

A before or enrichment plugin that returns `nil` drops the event. The SDK doesn't log the drop, and the callback doesn't fire for it.

To see each event after your other plugins have changed it, add an enrichment plugin that prints the event, and add it after your other plugins:

```swift
class InspectEventsPlugin: EnrichmentPlugin {
    override func execute(event: BaseEvent) -> BaseEvent? {
        if let data = try? JSONEncoder().encode(event),
           let json = String(data: data, encoding: .utf8) {
            print("[InspectEventsPlugin] \(json)")
        }
        return event
    }
}

let inspector = InspectEventsPlugin()
amplitude.add(plugin: inspector)
```

The printed JSON uses the same keys as the upload, such as `event_type`, `event_properties`, `device_id`, and `insert_id`. Compare it with what you expect. Remove the plugin with `amplitude.remove(plugin: 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 |

Requests go out every `flushIntervalMillis` (30 seconds by default), when the queue reaches `flushQueueSize` (30 events by default), when the app moves to the background, and when you call `flush()`. The SDK sends one request at a time.

The debug log shows that a request finished, but not its payload: `Debug: Successfully completed request` on success, or `Error: Response failed with code<status>: <response body>` on failure. To read the payload, use one of these methods:

- **Local proxy**: Use a proxy such as Proxyman or Charles. Install the proxy's root certificate on the simulator or device and turn on SSL proxying for `api2.amplitude.com` (or `api.eu.amplitude.com`).
- **Capture endpoint**: Point `serverUrl` at a local server that logs request bodies. The simulator shares your Mac's network, so `localhost` reaches it. On a device, use your Mac's address, and add an App Transport Security exception if the URL uses `http://`.

  ```swift
  let amplitude = Amplitude(configuration: Configuration(
      apiKey: "YOUR_API_KEY",
      serverUrl: "http://localhost:8080/2/httpapi"
  ))
  ```

  To accept the upload, the server must answer `200` with a body such as `{"code":200,"events_ingested":1,"payload_size_bytes":1,"server_upload_time":1793655976780}`.

The request body has this shape. Every event carries every field, with `null` for the ones the SDK didn't set. This excerpt omits most of the `null` fields:

```json
{
  "api_key": "YOUR_API_KEY",
  "client_upload_time": "2026-11-02T21:46:16.996Z",
  "events": [
    {
      "event_type": "Sign Up Clicked",
      "event_properties": { "plan": "starter" },
      "device_id": "EAB4A5BE-2294-4202-B9A8-EC96F02BA9D5",
      "idfv": "EAB4A5BE-2294-4202-B9A8-EC96F02BA9D5",
      "user_id": null,
      "session_id": 1793655976959,
      "time": 1793655976959,
      "event_id": 1,
      "insert_id": "B3E99093-7A57-4F37-914F-072F69224BDB",
      "library": "amplitude-swift/1.19.4",
      "platform": "iOS",
      "os_name": "ios",
      "os_version": "27.0",
      "device_manufacturer": "Apple",
      "device_model": "arm64",
      "carrier": "Unknown",
      "language": "en-US",
      "ip": "$remote",
      "version_name": "16.0",
      "app_version": null
    }
  ]
}
```

`"ip": "$remote"` tells Amplitude to use the request's IP address. The SDK doesn't set `app_version` for you. `version_name` holds the app's `CFBundleShortVersionString`. When you set `minIdLength`, the body also includes `"options": {"min_id_length": N}`. The body can also include a `request_metadata` object with SDK diagnostics. The `enableDiagnostics` option controls this object.

> **Note:** Compressed request bodies
>
> For Amplitude's own endpoints, the SDK gzip-compresses every request body and sends `Content-Encoding: gzip`. A proxy shows binary data unless it decodes gzip. When you set `serverUrl`, the SDK sends uncompressed JSON unless you also set `enableRequestBodyCompression: true`.

If no request appears:

| Cause | How to confirm | Fix |
| --- | --- | --- |
| The next scheduled upload hasn't run yet | No `Start flushing` line since the `track()` call. | Call `amplitude.flush()`. |
| The SDK is offline | `Debug: Skipping flush while offline.` `amplitude.configuration.offline` is `true`. | The SDK uploads when the network returns. If your app sets `offline`, set it to `false` and call `flush()`. |
| The host is unreachable or blocked | `Error: Conection failed with error: ..., marking offline` | Check the device's network, the `serverUrl` value, and App Transport Security for `http://` URLs. |
| Uploads failed too many times | `Error: Request failed more than 6 times, marking offline` | Fix the error in the earlier `Response failed` lines, then set `amplitude.configuration.offline = false` and call `flush()`. |

## Check what Amplitude received

### Read the upload result

Pass a `callback` to `Configuration`. The SDK calls the callback once for each event in a request, with the final event, a code, and a message:

```swift
let amplitude = Amplitude(configuration: Configuration(
    apiKey: "YOUR_API_KEY",
    callback: { event, code, message in
        print(event.eventType, code, message)
    }
))
```

To get the result for one event only, pass `callback` to `track(event:options:callback:)`. Both callbacks fire. For the callback's signature, go to [Callback](https://amplitude.com/docs/sdks/analytics/ios/ios-swift-sdk/reference#callback).

| `code` | `message` | Meaning | What the SDK does |
| --- | --- | --- | --- |
| `200` | `Successfully send event` | Amplitude accepted the event. | Removes the event from storage. |
| `400` | `Invalid API key: <your key>` | Amplitude doesn't recognize the key in this data center. | Drops every event in the request. |
| `400` | Server error text | Amplitude rejected this event, for example for invalid or missing fields. | Drops the events the server named, and queues the rest of the request again. |
| `413` | Server error text, for example `Payload too large` | The event is too large to upload on its own. | Drops the event. For a larger batch, the SDK splits it in half and retries first. |
| No call | n/a | `429`, `408`, `5xx`, a network error, opt-out, or a plugin dropped the event. | Retries uploads with backoff. Drops opt-out and plugin-dropped events before upload. |

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.

The SDK keeps per-event callbacks in memory only. If the app quits before the upload, the event still uploads on the next launch, but its per-event callback doesn't fire.

### 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 the inspection plugin's output.

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. Amplitude's chart queries can take a few minutes longer to include new events than User Lookup does.

