Unity SDK
openpush-unity is a receive-only Unity package over the native OpenPush iOS and Android SDKs. The natives register the device, carry identity and tags, post delivery receipts, present in-app messages, and run Live Activities (iOS) and Live Updates (Android). Like the native SDKs it has no send surface — messages are created through the REST API or the console.
When to use
Add this package to a Unity project targeting Android or iOS that should receive push from OpenPush. By default the package obtains the push token itself: Android fetches the FCM token through the native FCM module, and iOS captures the APNs token Unity receives. A project that already runs its own Firebase messaging on Android keeps it by choosing HostOwned delivery (Step 2).
Requirements
| Requirement | Value |
|---|---|
| Package version | 1.1.5 |
| Package name | ai.openpush.unity |
| Native SDKs (vendored in the package) | iOS 1.1.5, Android 1.1.5 |
| Minimum Unity | 2022.3 LTS |
| Android | Minimum API Level 24, Target API Level 34 or Automatic, EDM4U 1.2.189; IL2CPP + ARM64 recommended |
| iOS | Deployment target 16.0, built with Xcode 26 or newer (Swift 6.3.3 or newer) |
| Android, Automatic delivery | google-services.json in Assets/, or the Firebase project id, application id and API key in OpenPushSettings |
| Credentials | Your app id and the app's SDK key (X-OP-SDK-Key) |
The native SDKs ship inside the package: an in-package Maven repository for Android (Runtime/Plugins/Android/maven~, resolved by EDM4U) and static XCFrameworks for iOS (Runtime/Plugins/iOS/Native~, linked at export). No OpenPush artifact is downloaded from a public registry; EDM4U fetches only their third-party dependencies (firebase-messaging 24.0.2, Kotlin and AndroidX) from Maven Central and Google Maven.
The Editor and desktop players run an in-memory stub: calls are logged, nothing touches the network, and OpenPush.SdkVersion reads stub.
Step 1 — install the package
Installation is through a Git URL in Unity Package Manager. Install Git 2.14 or newer first and make sure git is on the system PATH; Package Manager shells out to the system client.
The repository is private, so every developer and CI runner needs read access before Unity can resolve it. Package Manager cannot show a credential prompt, so configure one of these up front:
- HTTPS — load a valid credential into the system Git credential helper.
- SSH — add an authorized key to
ssh-agentbefore launching Unity.
Do not embed an access token in the package URL or commit one to a package file. Confirm authentication outside Unity with git ls-remote; it must succeed without prompting.
Then Window → Package Manager → + → Add package from git URL:
Code
Or add it to Packages/manifest.json:
Code
Pin production projects to a release tag or a full commit SHA, never a moving branch, and commit packages-lock.json so every machine resolves the same revision.
An optional Feature Lab sample is available from the package's Samples section. It is copied into Assets/Samples on import and is not compiled until then. It forwards no tokens: the native SDKs obtain them.
Step 2 — Android: EDM4U and delivery mode
Open Window → OpenPush → Setup (also under Project Settings → OpenPush). Each row checks one requirement and offers a fix.
- Install EDM4U. On the External Dependency Manager (Android) row press Add EDM4U. It adds the OpenUPM scoped registry and
com.google.external-dependency-manager1.2.189toPackages/manifest.json. EDM4U creates and patches the Gradle templates itself; there is nothing to tick in Player Settings. - Resolve. The Android dependencies resolved row runs a forced resolve, which pulls the vendored OpenPush natives from the package's own Maven repository plus their dependencies. When the Editor loads with Android as the build target, OpenPush also forces a resolve after a package upgrade or a delivery-mode change: an upgraded package lives in a new
Library/PackageCache/ai.openpush.unity@<hash>folder, and the patched Gradle settings must point at it. After upgrading the package, make sure this row passes before you build. - Pick a delivery mode —
OpenPushSettings.AndroidPushDelivery, under Project Settings → OpenPush → Android delivery:- Automatic (default): the OpenPush FCM module fetches the token, renders notifications (with their action buttons) and reports taps. Your app adds no code. Put
google-services.jsoninAssets/(it takes precedence), or fill the three Firebase fieldsAndroidFirebaseProjectId,AndroidFirebaseApplicationIdandAndroidFirebaseApiKey. Without either, the device never gets an FCM token and the SDK only logs it; the Firebase configuration row checks for this. - HostOwned: your app owns FCM, for example with Firebase Unity Messaging. OpenPush's messaging service is removed from the merged manifest and the FCM module is not resolved. Your messaging code forwards to OpenPush — see Android HostOwned forwarding.
- Automatic (default): the OpenPush FCM module fetches the token, renders notifications (with their action buttons) and reports taps. Your app adds no code. Put
- API levels. Minimum 24, Target 34 or Automatic. The Android API levels row fixes them.
The adapter library declares android.permission.POST_NOTIFICATIONS and ships its own R8 keep rules, so release builds with Minify enabled need no entries in your project's ProGuard file, and the Android 13+ prompt needs nothing in your manifest.
Step 3 — iOS
There is no token step. The package observes Unity's APNs token notifications and hands the token to the native SDK (a token that arrives before Initialize is held and applied after it). It captures notification taps and foreground arrivals through the notification delegate Unity installs.
At export the package:
- copies the vendored XCFrameworks into the Xcode project and links them into UnityFramework (static, not embedded), and sets the deployment target to 16.0;
- adds the Push Notifications capability and the remote-notification background mode. The
aps-environmententitlement follows Unity's Development Build checkbox:developmentwhen ticked,productionwhen not; - injects a notification service extension when a settings asset exists and its
InjectServiceExtensionflag is on (see below); - injects a Live Activity widget when
OpenPushSettings.InjectLiveActivityWidgetis on.
Build the exported project with Xcode 26 or newer: the vendored frameworks' Swift interfaces need Swift 6.3.3 or newer.
Notification service extension
The extension is injected when an OpenPushSettings asset loads from Resources and its InjectServiceExtension flag is on; the field defaults to on. Without a settings asset nothing is injected. When injected, the export gains an OpenPushNotificationService target whose extension is the native OpenPushNotificationServiceHandler, linked against the vendored OpenPushCore and OpenPushNSE frameworks. iOS runs it for every OpenPush alert, including while the app is not running; it attaches rich media (image_url) and posts the received receipt, so a push that is never tapped is still counted.
- iOS runs one service extension per app. If your project already owns one, turn
InjectServiceExtensionoff. An export that already contains acom.apple.usernotifications.servicetarget is left alone either way. - The extension needs an App Group. OpenPush uses
OpenPushSettings.AppGroupIdentifier, orgroup.<your bundle id>.openpushwhen it is empty. Add that group to the App ID in your developer account before the first build, or manual signing fails with a profile that does not carry it. - No secret crosses the App Group boundary: the receipt route authenticates on the public SDK key, and the device credential stays in the app's Keychain.
Live Activity widget
The injected OpenPushLiveActivityWidget imports DefaultLiveActivityAttributes from the vendored OpenPushLiveActivities framework. Turn InjectLiveActivityWidget off when your project already owns a widget.
Step 4 — initialize
Create an OpenPushSettings asset — Assets → Create → OpenPush → Settings — and save it inside a Resources folder. Put your public SDK key in it; Initialize then needs only the app id.
Code
The credential closure returns the currently linked installation credential, or null before linking. To keep the key in code instead, call Initialize(appId, sdkKey); an explicit key always wins over the asset. With no key in either place, Initialize(appId) refuses through OnLog and OnRegistrationFailed rather than half-configuring. OpenPush.DefaultServerUrl is https://app.openpush.ai and is used when no server URL is given.
To start the SDK without code, add Add Component → OpenPush → Bootstrap to an object in your first scene and set App Id (or leave it blank to use OpenPushSettings.AppId).
Call Initialize first. Every call with a side effect (Register, Login, TrackEvent, User.* writes, PushSubscription.OptIn/OptOut, Notifications.RequestPermission, in-app and live-feature calls) throws InvalidOperationException until Initialize has run. LogLevel, OnLog, event subscriptions and read-only getters work at any time, so you can subscribe in Awake and initialize later. Each event keeps up to ten values raised before its first subscriber and replays them to it, except Notifications.ForegroundWillDisplay.
OpenPush.OnLog is the one diagnostic sink, native log lines included; OpenPush.LogLevel (Verbose by default) decides how much arrives.
Verify: OpenPush.IsInitialized is true; OpenPush.SdkVersion reads the native SDK version (1.1.5); OpenPush.WrapperVersion reads 1.1.5; the device appears in the console with wrapper unity/1.1.5.
Permission
Code
On Android 13+ the request goes through Unity's runtime-permission API and the native SDK is told the result; with fallbackToSettings, a request after a permanent denial opens the app's notification settings. Below Android 13 there is no runtime prompt and the call reports the current permission. RequestProvisionalPermission asks for iOS provisional authorization and reports false, changing nothing, elsewhere. The native SDK refreshes the cached permission and re-registers when it changes.
Identity
Code
Compute the identity-verification auth hash on your server, never in the client. A refused external id still registers the device anonymously and says why: Success == false with a non-null IdentityRejectedNote. OpenPush.User.Changed fires when the external id or the subscription id changes.
OpenPush.OnIdentityVerificationRequired(externalId) is raised when the app requires identity verification and the SDK holds an external id without an auth hash (for example one migrated from 1.1.4). Answer it with Login(externalId, authHash).
Tags, aliases and events
Code
Tags are device-level state kept by the native SDK; tag values are strings. The server caps aliases at 10 per user and refuses the reserved label external_id; refusals are logged. With identity verification on, tag and alias writes need a valid auth hash. The native SDKs persist tags, aliases, custom events and receipts and retry them on their own. See events.md for event limits.
Notifications and receipts
With Android Automatic delivery and on iOS, the native SDK renders notifications, posts the received, confirmed and clicked receipts, and raises the click and foreground events. The host forwards nothing.
Code
Both events carry Notification, an OpenPushNotification (title, body, image, launch URL, TTL, priority, action buttons), while Data stays the raw map. A click also reports ActionId (the tapped action button) and Url (the message's deep link, used as received). A tap that launched the app is replayed to the first Clicked subscriber.
ForegroundWillDisplay is answered once all handlers return: the notification is displayed unless a handler called PreventDefault(). With no handler it is displayed at once. Android waits up to one second for the handlers and then displays.
Notifications.ClearAll() and Notifications.Remove(messageId) remove delivered notifications. Notifications.SetBadgeCount(count) sets the iOS app icon badge and is a no-op elsewhere.
Android HostOwned forwarding
In HostOwned mode your own FirebaseMessagingService forwards to OpenPush:
Code
ProcessForeground runs the ForegroundWillDisplay handlers on the calling thread (call it from the main thread), posts received, and returns true when a handler called PreventDefault(). ProcessDisplayed posts confirmed. ProcessClick posts clicked and raises Clicked once. Read the action buttons to render from OpenPushNotification.From(data).Actions. OpenPush.MessageId(data) reads op_message_id from an IReadOnlyDictionary<string, string>; stringify a numeric message id before building that dictionary.
Subscription state
Code
OptedIn is true when the OS permission is granted, a provider token is present, and OptOut() was not called. OptOut() stops sends without deleting the subscription or the token; it survives a restart and is reversible.
PushSubscriptionState.Status is the native wire status: 1 subscribed, -2 opted out, 0 no permission, -18 never prompted, -19 prompt unanswered. On iOS the permitted value is the authorization bitmask (any positive value is subscribed). Android reports only -18/-19 and null otherwise; the Editor reports null.
In-app messages
On Android and iOS the native SDK fetches in-app messages, evaluates triggers, persists dismissals, presents messages in a native web view over the game, and posts impression, click and page receipts. Prompt, tag and outcome actions inside a message are carried out by the native in-app module.
Code
On both platforms Clicked reports Kind url, custom or close (a plain click when the action is none of these). ActionId is set only for custom clicks. Prompt, tag and outcome actions do not reach C# as their own kinds. See in-app-messages.md.
Live Activities (iOS) and Live Updates (Android)
Code
With Automatic delivery the native FCM service renders Live Updates itself, including while the app is killed. In HostOwned mode your messaging service calls ai.openpush.android.liveupdates.OpenPushLiveUpdates.handle(context, data, collapseKey) from the native live-updates module to render while the app is killed. A force-stopped Android app cannot receive FCM until the user launches it again. Remote start, update and end happen through the REST API or the console — see live-activities.md.
Credential storage
DeviceCredentialStore uses Android Keystore (AES-GCM) on Android and the Keychain on iOS. In the Unity Editor it is an in-memory, session-only store: a credential saved in play mode does not survive a domain reload or an Editor restart. Whenever secure storage is unavailable, saving fails closed: Save returns false, Load returns null, and credentials are never written as plaintext.
Upgrade from 1.1.4
Project changes
- Raise the project to Unity 2022.3 LTS.
- Android: set Minimum API Level 24 and Target API Level 34 or Automatic. The 1.1.4 library declared API 22, so devices on API 22 and 23 are no longer supported.
- Android: install EDM4U 1.2.189 and resolve (Step 2). After every package upgrade the resolved Gradle path contains the new
Library/PackageCachehash, so let the forced resolve run, or press Resolve, before building. - Android: choose a delivery mode. With
Automatic, remove your own token forwarding and provide the Firebase configuration. Projects that keep their own Firebase messaging chooseHostOwnedand keepRegister(token)plus theNotifications.Process*calls. - Android: remove any OpenPush keep rules you added to your ProGuard file; the package ships its own.
- iOS: build with Xcode 26 or newer (Swift 6.3.3 or newer). The deployment target stays 16.0.
- iOS: export into a clean folder (or choose Replace) the first time. Appending into a 1.1.4 export keeps the old Live Activity widget target, which references a removed source file and does not link the new framework.
Code changes
Removed (breaking):
OpenPush.Attach(...)— useOpenPush.Initialize.- The C# engine and its seams:
OpenPushClient,IOpenPushTransport,OpenPushRequest,OpenPushResponse,IOpenPushTokenCache,IOpenPushScheduler,OpenPushImmediateScheduler,IOpenPushPermissionBridgeand its helper interfaces (IOpenPushNotificationTypesBridge,IOpenPushProvisionalPermissionBridge,IOpenPushPermissionResolutionSource,IOpenPushPermissionPromptStateBridge),IOpenPushIamPresenter,IOpenPushHtmlIamPresenter,OpenPushIamPresentationContext, the engine classOpenPushInAppMessages(OpenPush.InAppMessagesis unchanged),InAppActionParser,InAppTriggerEngine,OpenPushInAppState,OpenPushPayload,OpenPushSessionStore,OpenPushSessionState,OpenPushEnvironmentandUnityWebRequestTransport.
Changed behaviour:
- Calls with side effects before
InitializethrowInvalidOperationException; tags set beforeInitializeno longer ride the first registration. OpenPush.SdkVersionreturns the native SDK version; the package version isOpenPush.WrapperVersion.- In-app click events report only
url,customandclosekinds;ActionIdis set only forcustomclicks. OpenPush.Flush()andOpenPush.InAppMessages.ResetDisplayState()are obsolete no-ops that only log.
Added:
OpenPush.OnIdentityVerificationRequired,OpenPush.WrapperVersion,PushSubscriptionState.Status,OpenPush.PushSubscription.ChangedWithPrevious,OpenPush.Notifications.SetBadgeCount(iOS; a no-op elsewhere),OpenPushLiveUpdateResult.Stale.OpenPushSettingsfieldsAndroidPushDelivery(Automatic|HostOwned),AndroidFirebaseProjectId,AndroidFirebaseApplicationId,AndroidFirebaseApiKeyandReloginWithoutAuthHash.
State migration
The first launch of a 1.1.5 build moves the 1.1.x state kept in PlayerPrefs into the native SDK, once:
- The installation credential is reused — keep passing the same
DeviceCredentialStoreclosure — so the server keeps the same subscription id and no duplicate device appears. - Tags, aliases and an explicit opt-out are carried over. In Android
HostOwnedmode the stored provider token is registered again; Automatic delivery and iOS fetch a fresh one. - The external id is logged back in without an auth hash when
OpenPushSettings.ReloginWithoutAuthHashis on (the default). Apps that use identity verification turn it off: OpenPush then raisesOpenPush.OnIdentityVerificationRequired(externalId), leaves the device anonymous, and your handler callsLogin(externalId, authHash)with a hash from your server. - In-app dismissals are not migrated, so a dismissed message may show once more.
A failed migration is logged and retried on the next launch.
Migrating hosts that use the compatibility facade: see the compatibility migration guide in the package root.
Limits
- No send surface, on any platform.
- No notification inbox and no consent gate.
- Git-URL installation only, from a private repository.
- Editor play mode runs a stub: no real push delivery and no persistent credential storage.
Troubleshooting
| Symptom | Fix |
|---|---|
| Package Manager reports Git cannot be found | Install Git or add it to PATH, then restart Unity |
terminal prompts disabled during resolve | Configure a credential helper or SSH agent, verify with git ls-remote, reopen Unity |
Initialize refuses through OnLog | Create the settings asset inside a Resources folder with the SDK key, or pass sdkKey |
InvalidOperationException from an OpenPush call | Call Initialize before any call with a side effect |
Gradle cannot find ai.openpush:openpush-android:1.1.5, or points at an old package folder, after an upgrade | Press Resolve on the Android dependencies resolved setup row |
checkDebugAarMetadata asks for compileSdkVersion 34 | Set Target API Level to 34 or Automatic |
Sealed classes are not supported as program classes during the Gradle build | The vendored natives are stale; update the package |
| No FCM token in Automatic mode | Add google-services.json or the three Firebase fields; the Firebase configuration row and the native log say which is missing |
| Device registered but nothing arrives on Android | Upload the FCM service account under Android — see platform-setup-fcm.md |
| Xcode cannot read the OpenPush Swift interfaces | Build with Xcode 26 or newer (Swift 6.3.3 or newer) |
| Old Live Activity widget fails to build after upgrading | Export into a clean folder, or choose Replace |
| Manual signing fails on the App Group | Add the App Group (group.<bundle id>.openpush or your AppGroupIdentifier) to the App ID |
| Identity reported anonymous | Identity verification is on and the auth hash is missing or wrong: compute the HMAC on your server and pass it to Login |
FAQ
Does this package fetch the push token for me?
Yes, by default. Android fetches the FCM token through the native FCM module (Automatic delivery), and iOS forwards the APNs token Unity receives. Only Android HostOwned mode calls Register(token).
Can I use it with Firebase Unity Messaging?
Yes. Set AndroidPushDelivery to HostOwned: OpenPush removes its own messaging service so yours is the only one, and you forward tokens and payloads to it.
Why does SdkVersion differ from the package version?
SdkVersion is the native SDK version; WrapperVersion is the Unity package version.
Related
- quickstart.md
- sdk-android.md
- sdk-ios.md
- platform-setup-fcm.md
- platform-setup-apns.md
- users-and-subscriptions.md
- live-activities.md
- events.md
- in-app-messages.md
- ../api-handbook/03-subscriptions-users.md
- ../api-handbook/10-in-app-messages.md