iOS SDK
openpush-ios is a receive-only Swift package. It registers an APNs device token with OpenPush, carries the user's identity and tags, drives Live Activities, and posts delivery receipts. It contains no message creation and no send API.
When to use
Add this SDK to any iOS app that should receive push from OpenPush. Pair it with a Notification Service Extension — on iOS that extension is not optional decoration, it is what makes images and confirmed receipts work at all.
Requirements
| Requirement | Value |
|---|---|
| SDK version | 1.1.5 |
| Minimum iOS | 16 |
| Swift tools | 6.0 |
| Package manager | Swift Package Manager; podspecs exist in the repository but are not published to CocoaPods trunk |
| Capabilities | Push Notifications enabled on the app target |
| Extension | A Notification Service Extension target |
| Credentials | Your app id and the app's SDK key (X-OP-SDK-Key) |
| Server | Nothing to choose — the API lives at https://app.openpush.ai. You do need an APNs p8 key on the app — see platform-setup-apns.md |
Install with Swift Package Manager; podspecs exist in the repository but are not published to CocoaPods trunk. There is no Carthage support.
The SDK key is public by design and ships in the app binary. The REST API key must never appear in an app. See security-and-limits.md.
Products
| Product | Link it to |
|---|---|
OpenPush | the app target — registration, identity, receipts |
OpenPushCore | shared configuration, payload parsing, Keychain credential storage; usable from both targets |
OpenPushNSE | the Notification Service Extension target |
OpenPushLiveActivities | the widget extension target (not OpenPush) |
| The compatibility facade product | opt-in source compatibility for migrating hosts — see Compatibility facade |
Compatibility facade
The compatibility facade is an opt-in product for migrating hosts. It accepts the API spelling of the previous provider's SDK, so existing call sites compile against OpenPush unchanged. It is a source-compatibility layer only. See the migration guide.
Step 1 — install
In Xcode choose File → Add Package Dependencies and enter the package repository URL, then select the 1.1.5 version rule. Link OpenPush to the app target and OpenPushNSE to the Notification Service Extension target.
For a Package.swift consumer:
Code
1.1.5 is the version the Unity package vendors; its tag is published with the release.
Step 2 — initialize
Put the SDK key in the app's Info.plist under OpenPushSDKKey (the app id key is OpenPushAppID), then:
Code
serverURL already defaults to https://app.openpush.ai, so the argument above is optional and shown only to make the target explicit. To keep the key in code instead, use initialize(appId:sdkKey:…) — an explicit key always beats the bundled one. With no key in either place, initialize refuses rather than half-configuring.
attach(configuration:credential:transport:) remains available for hosts that build their own OpenPushConfiguration.
There is an initialize(_:withLaunchOptions:) overload for migrating hosts' source compatibility. The launch options are accepted and ignored — OpenPush does not swizzle and does not install any delegate.
When authorization already exists, a normal initialize asks iOS to re-issue the APNs token without prompting, so a rotated token resynchronizes.
Verify: OpenPush.isInitialized is true and OpenPush.onLog reports no refusal.
Step 3 — forward the APNs token
The SDK never swizzles your app delegate. Forward both callbacks yourself:
Code
OpenPush.register(token:externalID:) and OpenPush.hexToken(_:) are available if you manage the conversion yourself.
Transport errors and 5xx responses retry at +2 s and +8 s; onRegistrationFailed fires only after the final attempt. A scope_conflict response is surfaced as an instruction to reset the device, not as a connection failure. Forwarded APNs failures use the same hook, with diagnostics that name a missing Push Notifications capability and simulator limitations.
Verify: OpenPush.onRegistered fires and the device appears in GET /v1/apps/{app_id}/subscriptions.
Step 4 — request permission
Code
fallbackToSettings deep-links to the OS notification settings once iOS stops showing the prompt.
Provisional (quiet) authorization
iOS is the only OpenPush SDK with a soft prompt:
Code
This requests provisional plus alert, sound and badge, so notifications are delivered quietly to Notification Center with no prompt. Provisional is a positive status carrying bit 64 — a provisional device is subscribed and fully targetable.
Status reporting
Every registration reports the iOS authorization bitmask. The bits are 1 badge, 2 sound, 4 alert, 8 CarPlay, 16 critical, 32 app settings, 64 provisional, 128 announcement, 256 time-sensitive; any positive value means subscribed. 0 is denied, -18 never prompted, -19 prompt unanswered, -2 explicitly opted out. The full table is in users-and-subscriptions.md.
The SDK recomputes authorization at launch and on foreground, and re-registers only when the value changed — so a change made in Settings reaches the server on the next launch. UIKit apps are observed automatically; a host without UIKit should call OpenPush.Notifications.onAppForegrounded().
Watch changes with addPermissionObserver (returns a UUID) and removePermissionObserver.
Step 5 — identity
Code
A login before the APNs token arrives is kept and sent with the next registration. logout re-registers with a present-and-empty external_id, which is what detaches the account.
Compute authHash on your server. The external ID persists across a cold start; the auth hash is a bearer proof and is deliberately memory-only, so call login again at launch. See users-and-subscriptions.md.
Step 6 — tags and aliases
Code
Single-key forms (addTag, removeTag, addAlias, removeAlias) and getTags() exist too.
Rules that matter:
- Tag values are strings only.
- Deleting is an empty-string value, and a removed key keeps riding as
""so a re-registration cannot resurrect it. - Writes persist immediately and share a fixed 300 ms window, so a burst becomes one whole-map registration. Backgrounding flushes pending writes.
- Client-side alias rules: an empty label is dropped, the label
external_idis reserved and refused locally, and going past 10 aliases produces a warning. - The server also caps aliases at 10 per user and names refused labels on a successful response rather than failing the request.
- With identity verification enabled, a tag or alias write without a valid auth hash comes back rejected and is logged.
Step 7 — the Notification Service Extension
This step is mandatory for two features, not one.
Add a Notification Service Extension target, link OpenPushNSE (and OpenPushCore), and give the extension access to the same shared Keychain access group as the app so it can load the same installation credential. Never copy a device credential into UserDefaults or an App Group plist.
Why it is required
OpenPush sets mutable-content: 1 on every alert push, unconditionally — not only on pushes that carry an image. The extension is the only iOS code that runs when a notification arrives at a backgrounded app, so gating mutable-content on the presence of an image would silently forfeit the confirmed receipt on every plain-text campaign. Because it is always set, the extension always runs, and both of these work:
- Images. OpenPush does not build an
apsattachment. The image travels as a top-level custom keyimage_url, and your extension downloads it and attaches it. Without an extension, images never appear. - Confirmed receipts. The extension posts the
confirmedstage for notifications the app never saw, which is the difference between "the provider accepted it" and "the device displayed it".
Receipts from the extension
Code
make builds a bounded request to /v1/ingest with the SDK key already attached. The extension imports Foundation only and exposes no registration, console-management or send API.
Action buttons from the extension
Code
Call this before delivering mutable content. It registers the button labels configured on the message as a UNNotificationCategory and passes the tapped action identifier through to the app delegate. OpenPush-owned categories are bounded to the 32 most recently used label sets; host-registered categories are never touched.
Step 8 — foreground and click handling
Code
processForeground returns the presentation options and posts received. processClick posts clicked. Listeners:
Code
Both return a UUID for the matching remove…. ClickEvent.actionId is nil for the default open action and carries the button identifier otherwise.
App-side receipts can also be posted directly:
Code
Receipts are deduplicated on type|messageId for successful posts only; a failed attempt stays retryable.
iOS payload keys
The sender folds op_apns_* options into Apple's aps dictionary and strips them from the custom keys, so they do not reach your code. What your app sees is:
| Key | Meaning |
|---|---|
op_message_id | the id to pass to receipt — read it with OpenPush.messageID(from:) |
op_actions | action buttons, max 3 — OpenPush.actions(from:) |
op_action_category | the derived UNNotificationCategory identifier |
image_url | the image your extension fetches and attaches |
deep_link | the destination, when the message sets one |
op_render_truncated | "1" if the server trimmed the payload to fit |
The sender-side APNs options — subtitle, sound, badge, category, thread id, interruption level, relevance score — are covered in sending-messages.md.
Live Activities
The SDK has full ActivityKit support. Define one shared attributes type for the app and the widget:
Code
setup observes ActivityKit tokens, dismissals and confirmed receipts for you. setupDefault(options:) and startDefault(_:attributes:content:) cover the untyped default activity.
A host that manages tokens itself uses the direct hooks: enter(_:withToken:) and the activityType overload, exit(_:), setPushToStartToken(activityType:token:), removePushToStartToken(_:), plus ActivityAttributes-typed generics on iOS 17.2. Every request is queued on disk until registration or a transient outage clears.
Link OpenPushLiveActivities — not OpenPush — to the widget extension, add NSSupportsLiveActivities to the app plist, and use .openpushWidgetURL in the widget. For activity types that need a higher remote-update budget, also add NSSupportsLiveActivitiesFrequentUpdates; the SDK observes both settings and synchronizes them as device tags (device_live_activities_enabled and device_frequent_pushes).
Count widget taps from the app's URL handler:
Code
Server-side start, update and end live in live-activities.md.
Custom events
Code
Names are 1–128 characters matching [a-zA-Z0-9_\-. ]+; properties must be JSON-safe and at most 2048 bytes. Events are persisted while offline and sent in batches of up to 50 after the same 300 ms window. See events.md.
Subscription state and opt-out
Code
The accessor is live: a value stored before registration reflects the later id, token and status. Call snapshot() for an immutable, equatable value. Opt-out intent persists across process death and does not delete the subscription or the token.
iOS is the only OpenPush SDK whose subscription state carries status. Android and Unity expose id, token and optedIn only.
App sessions
Cold launches and foreground transitions send a throttled app-session ping, at most one per 30 minutes, persisted across relaunches. It feeds session counts and the activity histogram behind best-hour delivery.
Limits
- No send surface. Sending is REST or console only.
- No swizzling and no delegate installation — you forward every callback.
- No badge-count setter. Badge appears only as an authorization bit and a presentation option.
- No consent gate: the compatibility facade's
consentRequiredandconsentGivenare compile-time refusals that name the replacement, not silent no-ops. No notification inbox either — no inbox API exists and the compatibility facade declares none. - In-app messages are supported.
OpenPush.InAppMessagesis real — aWKWebViewpresenter, trigger state, and the impression/click receipt path — and the compatibility facade'sInAppMessagesresolves onto it rather than refusing. See in-app-messages.md and the in-app messages API. - Tag values are strings, always.
- No CocoaPods or Carthage distribution.
- Critical alerts are not supported by the sender: the critical sound dictionary is never built, and
interruption-level: criticalis deliberately excluded. - Without a Notification Service Extension you get no images and no confirmed receipts for backgrounded arrivals.
- Nothing in the
OpenPushmodule is marked deprecated, despite what some in-repo Live Activities documentation claims.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
initialize refuses through onLog | No OpenPushSDKKey in Info.plist and none passed in code | Add the plist key or use initialize(appId:sdkKey:) |
| No token ever arrives | The app delegate callbacks are not forwarded | Implement both didRegisterForRemoteNotifications… methods; OpenPush never swizzles |
| Push works in TestFlight but not the App Store build (or the reverse) | Sandbox and production tokens in one app | The device's own sandbox flag beats the app setting — see platform-setup-apns.md |
| Images never appear | No Notification Service Extension, or it does not fetch image_url | Add the extension; OpenPush sends the image as a top-level key, not an aps attachment |
| Provider Accepted is high, Confirmed Receipt is zero | The extension is missing or is not posting receipts | Use OpenPushReceiptRequest.make from the extension with a shared-Keychain credential |
| Action buttons render without labels | OpenPushNotificationActions.apply not called in the extension | Call it before delivering mutable content |
| Every send fails with a configuration error and no device is touched | An APNs provider-token problem — wrong key type, wrong key id, or an expired token | Fix the credential; a configuration fault stops the fan-out rather than marking devices unreachable |
| Identity shows as anonymous | Identity verification on, auth hash missing or wrong | Compute the HMAC on your server and pass it as authHash at every launch |
| Simulator receives nothing | Remote push registration is limited there | Test on a device |
FAQ
Can I install with CocoaPods? Not from CocoaPods trunk. Install with Swift Package Manager; podspecs exist in the repository but are not published to CocoaPods trunk.
Is the Notification Service Extension really required? For images and for confirmed receipts on backgrounded arrivals, yes. Plain alerts will still display without it, but you lose the receipt stage that distinguishes "displayed" from "accepted by Apple".
Why is mutable-content set on a plain-text push?
Because the extension is the only code that runs on a backgrounded arrival. Setting it only when an image is present would silently drop the confirmed receipt on text campaigns.
Does provisional authorization count as subscribed?
Yes. Provisional carries bit 64, and every positive status value is subscribed and targetable.
Which version is current?
1.1.5.
Related
- quickstart.md
- platform-setup-apns.md
- live-activities.md
- users-and-subscriptions.md
- sending-messages.md
- events.md
- Migration guide
- in-app-messages.md
- ../api-handbook/03-subscriptions-users.md
- ../api-handbook/08-live-activities.md
- ../api-handbook/10-in-app-messages.md