> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt
# PostHog Flutter SDK migration guides
Canonical URL: https://posthog.com/docs/libraries/flutter/migration
## On this page
- [Requirements](#requirements)
- [Breaking API changes](#breaking-api-changes)
- [Behavior changes](#behavior-changes)
- [New features](#new-features)
- [Upgrade checklist](#upgrade-checklist)
## Migrating from posthog\_flutter 5.x to 6.0
## Requirements
| | 5.x | 6.0 |
| --- | --- | --- |
| iOS deployment target | 13.0 | 15.0 |
| macOS deployment target | 10.15 | 11.0 |
| Android `minSdkVersion` declared by the plugin | 21 | 23 |
| iOS and macOS dependency manager | CocoaPods or Swift Package Manager | Swift Package Manager only |
| Flutter SDK | 3.27 | 3.41 |
posthog\_flutter 6.0 needs the Flutter SDK 3.41 or later on every platform: `flutter pub get` fails on an older Flutter, including in Android-only apps. The Dart minimum (`>=3.6.0`) is unchanged. Only iOS and macOS builds need Swift Package Manager.
### iOS and macOS
Raise your app's deployment targets. A lower target fails the build. Set **Minimum Deployments** for the `Runner` target in Xcode (**Runner > General > Minimum Deployments**), which sets `IPHONEOS_DEPLOYMENT_TARGET` and `MACOSX_DEPLOYMENT_TARGET`.
If you keep a `Podfile` for other plugins, raise its platform too:
```ruby
# ios/Podfile
platform :ios, '15.0'
# macos/Podfile
platform :osx, '11.0'
```
### Move from CocoaPods to Swift Package Manager
posthog\_flutter 6.0 has no podspec. On iOS and macOS, it installs only through Swift Package Manager.
**If you ignore this, the build stops.** With Swift Package Manager turned off, `flutter build` exits with `Plugin posthog_flutter is only compatible with Swift Package Manager`.
1. Use Flutter 3.41 or later. The plugin's `Package.swift` depends on the `FlutterFramework` package, which Flutter generates starting with 3.41.
2. Turn Swift Package Manager on if it's off. It's on by default from Flutter 3.44. On 3.41 to 3.43, or if you turned it off, run:
```sh
flutter config --enable-swift-package-manager
```
3. Run your app. Flutter adds Swift Package Manager to the Xcode project on its own.
Other plugins that only support CocoaPods keep working: Flutter installs them through your `Podfile` and installs posthog\_flutter as a Swift package. Keep the `Podfile` in that case.
If every plugin in your app supports Swift Package Manager, you can remove CocoaPods. This is optional. Follow [Flutter's steps](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#how-to-remove-cocoapods-integration).
**Add-to-app:** `flutter build swift-package` still installs a Flutter module's plugins with CocoaPods on current stable releases, so it stops with `Unable to find a specification for 'posthog_flutter' depended upon by 'FlutterPluginRegistrant'` because posthog\_flutter has no podspec. Flutter fixed this in [flutter/flutter#184590](https://github.com/flutter/flutter/issues/184590), which isn't in a stable release yet. If you embed Flutter as a module in an iOS or macOS app, stay on posthog\_flutter 5.x until it is. On Flutter 3.44 or later, you can instead embed Flutter in an iOS or macOS app from a Flutter app project (`flutter create`, not `-t module`) by following Flutter's Swift Package Manager add-to-app steps for [iOS](https://docs.flutter.dev/add-to-app/ios/project-setup) or [macOS](https://docs.flutter.dev/add-to-app/macos/project-setup); this works with posthog\_flutter 6.
### Android
`posthog-android` has required `minSdkVersion` 23 since 3.39.0, so an app on 21 or 22 already could not build with this plugin. The plugin now declares the same value, and you have nothing to change.
### Native SDKs and Xcode
| | 5.x | 6.0 |
| --- | --- | --- |
| `posthog-ios` | `>= 3.92.3, < 4.0.0` | `>= 4.0.0, < 5.0.0` |
| `posthog-android` | `[3.75.0, 4.0.0)` | `[4.0.0, 5.0.0)` |
| Xcode | Not set by the plugin | 26 or later |
Don't add these native SDKs to your app separately at a different major version.
## Breaking API changes
These all fail at compile time unless noted otherwise.
### `getFeatureFlagResult(sendEvent:)` is now `sendFeatureFlagEvent:`
The parameter is also now `bool?`. See [the default change](#getfeatureflagresult-follows-your-config) below.
```dart
// Before
await Posthog().getFeatureFlagResult('my-flag', sendEvent: false);
// After
await Posthog().getFeatureFlagResult('my-flag', sendFeatureFlagEvent: false);
```
### `PostHogConfig.apiKey` is removed
Read `projectToken` instead. The constructor already takes the token as its first argument.
```dart
// Before
final token = config.apiKey;
// After
final token = config.projectToken;
```
### `com.posthog.posthog.API_KEY` is removed
Apps that start the SDK from `AndroidManifest.xml` or `Info.plist` must rename the key. **This does not fail at build time.** With only the old key, the SDK logs an error and does not start, so the app sends no data.
```xml
```
```xml
com.posthog.posthog.API_KEY
com.posthog.posthog.PROJECT_TOKEN
```
### `Posthog().enable()` and `disable()` are removed
```dart
// Before
await Posthog().disable();
await Posthog().enable();
// After
await Posthog().optOut();
await Posthog().optIn();
```
### `PostHogConfig.sendFeatureFlagEvents` is removed
```dart
// Before
config.sendFeatureFlagEvents = false;
// After
config.sendFeatureFlagEvent = false;
```
### `Posthog().getFeatureFlagPayload()` is removed
`getFeatureFlagPayload()` never captured `$feature_flag_called` on iOS, macOS, Android, Windows or Linux. Pass `sendFeatureFlagEvent: false` to keep it that way. Without it, the replacement call captures the event when your config allows it.
```dart
// Before
final payload = await Posthog().getFeatureFlagPayload('my-flag');
// After
final result = await Posthog().getFeatureFlagResult(
'my-flag',
sendFeatureFlagEvent: false,
);
final payload = result?.payload;
```
### `PostHogSessionReplayConfig.debouncerDelay` is removed
```dart
// Before
config.sessionReplayConfig.debouncerDelay = const Duration(seconds: 2);
// After
config.sessionReplayConfig.throttleDelay = const Duration(seconds: 2);
```
### `PostHogDataMode.cellular` is removed
It always behaved like `any`, so sending does not change.
```dart
// Before
config.dataMode = PostHogDataMode.cellular;
// After
config.dataMode = PostHogDataMode.any;
```
### `toMap()` and `PostHogFeatureFlagResult.fromMap()` are removed
They serialized the internal platform-channel format and have no replacement. `toMap()` is gone from `PostHogConfig`, `PostHogBootstrapConfig`, `PostHogLogsConfig`, `PostHogRageClickConfig`, `PostHogSessionReplayConfig`, `PostHogErrorTrackingConfig` and `PostHogExceptionStepsConfig`. To build a result, for example in a test, call the `PostHogFeatureFlagResult` constructor, which stays public.
### `beforeSend` callbacks are synchronous
`BeforeSendCallback` is now `PostHogEvent? Function(PostHogEvent)`, and `BeforeSendLogCallback` is `PostHogLogRecord? Function(PostHogLogRecord)`. An `async` callback, or one declared to return `FutureOr`, is a compile error. Load what it needs ahead of time and read it synchronously.
```dart
// Before
config.beforeSend = [
(event) async {
final allowed = await loadAllowList();
return allowed.contains(event.event) ? event : null;
},
];
// After
final allowed = await loadAllowList();
config.beforeSend = [
(event) => allowed.contains(event.event) ? event : null,
];
```
### Platform interface changes
These affect only code that calls, extends or fakes `PosthogFlutterPlatformInterface`, `PosthogFlutterIO` or `PosthogFlutterWeb` directly, such as test mocks. `Posthog().optIn()` and `optOut()` are unaffected.
- `enable()` and `disable()` are now `optIn()` and `optOut()`.
- `getFeatureFlagPayload()` is removed.
- `getFeatureFlagResult({required String key, bool sendEvent = true})` is now `getFeatureFlagResult({required String key, bool? sendFeatureFlagEvent})`.
- `capture`, `screen`, `identify`, `setPersonProperties`, `alias`, `group` and `captureException` take an `options` map.
- `captureAi()` is new. The base class throws `UnimplementedError` unless you override it.
- `isFeatureEnabled()` takes `defaultValue`.
- `reloadFeatureFlags()` returns a `Future`.
- `setAppLanguage(String? language)` is new. The base class does nothing unless you override it.
```dart
// Before
@override
Future disable() async {}
// After
@override
Future optOut() async {}
```
### `PostHogConfig.onFeatureFlags` is removed
Call `Posthog().onFeatureFlags()` before `setup()`, which also covers the first load. The callback receives a `PostHogFeatureFlagsLoaded`, with `flags`, `variants` and `errorsLoading`, and `reloadFeatureFlags()` now returns a `Future`.
```dart
// Before
final config = PostHogConfig('', onFeatureFlags: () {
// ...
});
// or
config.onFeatureFlags = () {
// ...
};
await Posthog().setup(config);
// After
Posthog().onFeatureFlags((loaded) {
if (!loaded.errorsLoading) {
// Use loaded.variants
}
});
await Posthog().setup(config);
```
## Behavior changes
### Events go to capture v1
**This is a silent change if you use a reverse proxy or self-host.** On iOS, macOS, Android, Windows and Linux, events go to `/i/v1/analytics/events` instead of `/batch/`, and `captureAi` events go to `/i/v1/ai/events`. PostHog Cloud and the managed reverse proxy already serve the new paths. Most apps upgrade without code changes, and nothing here produces a compile error.
| Platform | Sent by | 6.0 |
| --- | --- | --- |
| iOS and macOS | posthog-ios 4.x | capture v1 |
| Android | posthog-android 4.x | capture v1 |
| Windows and Linux | posthog\_flutter's own Dart client | capture v1 |
| Web | the posthog-js your app loads | unchanged |
Web doesn't move to capture v1 in this release. It keeps posthog-js and its current endpoints. On web, `options` are sent as the legacy option properties, and `captureAi` sends nothing.
For the changes every capture v1 SDK shares, such as event options, limits and per-event results, see [Upgrading to capture v1 SDKs](https://posthog.com/docs/libraries/capture-v1). On iOS, macOS and Android, capture v1 comes from the native SDKs, so the Capture v1 sections of the [iOS](https://github.com/PostHog/posthog-ios/blob/main/MIGRATION.md#capture-v1) and [Android](https://github.com/PostHog/posthog-android/blob/main/docs/migration-3.x-to-4.0.md#endpoints) guides apply there too. Windows and Linux are covered [below](#capture-v1-on-windows-and-linux).
On every platform except web, an event's properties are now the SDK's, then registered properties, then the ones you pass, so your value wins over the SDK's for that event. In 5.x, the SDK's values won.
#### Checklist
You need to change code if your app does any of these:
- sends events to a self-hosted PostHog or a reverse proxy that doesn't serve `/i/v1/analytics/events` and `/i/v1/ai/events`
- sends events through a reverse proxy set up from PostHog's proxy guides before 29 October 2026, which strips `Authorization`
- identifies users with a distinct ID over 200 UTF-8 bytes, such as a JWT
- captures events whose name is over 200 UTF-8 bytes
- sets `projectToken` to a personal API key (`phx_...`)
- reads `$session_id` or `$window_id` from the raw request body, for example in a proxy
- relies on a `Retry-After` longer than 30 seconds
- registers or passes a property with the same key as one the SDK sets, such as `$os` or `$is_identified`, and expects the SDK's value to win
- on any platform except web, drops the event's `options` in `beforeSend` by returning a new `PostHogEvent` or replacing `event.options`
- on Windows or Linux, alerts on the SDK's debug log lines
- on Windows or Linux, calls `exit()` without awaiting the calls before it or `Posthog().close()`
#### Reverse proxies and self-hosting
Before you upgrade, make your own proxy:
- forward `/i/v1/analytics/events` and `/i/v1/ai/events`
- pass `Authorization` (the project token, sent as `Bearer `), every `PostHog-*` header, `Content-Type`, `Content-Encoding` and `User-Agent` through unchanged. PostHog answers `401` without `Authorization` and `400` without the others, and the batch is dropped.
- if your proxy strips other credentials, keep `Authorization` values that start with `Bearer phc_`, `Bearer phs_` or `Bearer phx_`, plus the exact `Bearer ` of an older key without a prefix
- accept request bodies up to 20 MB. AI requests are often larger than 1 MB, the default limit in nginx and ingress-nginx.
- pass PostHog's response body through unchanged, because a 2xx whose body isn't PostHog's `{"results": {...}}` drops the events instead of retrying them
PostHog's managed reverse proxy already does this and needs no change. A proxy set up from PostHog's [proxy guides](https://posthog.com/docs/advanced/proxy) before 29 October 2026 strips `Authorization`, so PostHog answers `401` and the events are dropped. Update it from the current guide.
The SDK doesn't fall back to `/batch/`. On iOS, macOS, Android, Windows and Linux, a 404 is retried, events stay queued until the route is served, and the SDK prints one warning, also without `debug`. Other statuses that aren't retried, such as 401, 403 or 405, drop the batch. Web keeps posthog-js, which doesn't send to these routes. For iOS and Android details, see the [iOS](https://github.com/PostHog/posthog-ios/blob/main/MIGRATION.md#hosts-without-capture-v1) and [Android](https://github.com/PostHog/posthog-android/blob/main/docs/migration-3.x-to-4.0.md#hosts-without-capture-v1) guides.
### Feature flag payloads
On iOS, macOS and Android, a malformed (such as a plain-text `hello`), empty or whitespace-only payload from `getFeatureFlagResult()` is now `null` instead of the raw string. On Android, a JSON `null` payload is also `null` instead of the string `"null"`.
On iOS and macOS, string values in `PostHogBootstrapConfig.featureFlagPayloads`, such as `"123"`, stay strings instead of being decoded to numbers or booleans.
A flag whose variant is an empty string reads as disabled on iOS, macOS, Android, Windows and Linux, as it already did on web. `isFeatureEnabled()` returns `false`, with or without `defaultValue`, `getFeatureFlagResult()` returns `enabled: false`, `onFeatureFlags` listeners and `reloadFeatureFlags()` leave the flag out, and so does `$active_feature_flags` on captured events. `variant`, `getFeatureFlag()` and the event's `$feature/` property still return `""`.
### `beforeSend` on iOS, macOS and Android
**This is a silent change.** Callbacks now get the event the native SDK built, including its system properties, such as `$device_type` and `$session_id`, registered properties and options, such as the `process_person_profile: false` the SDK adds while person processing is off. Their changes are final. In 5.x they saw only the properties and options you passed, so a callback that loops over or strips every property now also touches the SDK's. `PostHogEvent` also has `uuid`, `distinctId` and `timestamp`, set on every platform except web. A callback can change the distinct ID and timestamp, and the event keeps its uuid.
**Person processing is decided before the callbacks run.** In 5.x, a callback that removed `$set` prevented a person profile. Now the event is still person-processed, and so are later events. Return `null` to drop an event that must not create a profile. A callback that adds `$set` to an anonymous event must also set the `process_person_profile: true` option.
Nested maps are `Map`, and a non-empty list of maps is `List