iOS SDK
Push notifications
Notify people about support replies through Apple Push Notification service (APNs), refresh unread state in the foreground, and open chat when they tap a notification. Keep your app’s existing notification delegate and permission flow throughout setup.
How push works with Reception
Complete Get started first. Push needs a correctly signed app, notification authorization, an APNs device token forwarded to ReceptionKit, and matching credentials in the dashboard. Chat remains usable without notification permission. You can prepare the app hooks now and complete dashboard credentials later. ReceptionKit also refreshes unread state without push; see Automatic unread checks.
Your app owns permission requests, APNs registration, delegate routing, and foreground presentation. ReceptionKit stages the token, synchronizes it after support usage starts, recognizes Reception payloads, and requests chat presentation. The backend sends alert notifications for agent replies, including replies from Telegram and review cards.
Offer cards can also send a push when an owner enables offer pushes for the app.
Push delivery is best-effort. A saved reply or configured key does not prove a notification reached a phone.
Set up Xcode
Open your app target’s Signing & Capabilities and add Push Notifications.
Enable push for your app's identifier in Apple Developer (Apple calls it an App ID) and use a provisioning profile that includes it.
Check the target’s entitlements file and
CODE_SIGN_ENTITLEMENTSmapping. Verify that the signed app hasaps-environment:developmentfor development provisioning orproductionfor distribution. Let signing determine the entitlement.
These steps follow Apple’s APNs registration guide. Reception sends alert pushes; this integration does not require Background Modes → Remote notifications. Dashboard credentials do not replace app signing or entitlements.
Keep permission under your app’s control
ReceptionKit never requests notification permission. Reuse your existing permission timing and UI. Do not add a launch prompt just to integrate Reception. If your app has no permission flow, wire the non-prompting hooks below first; introduce authorization separately at an intentional point in your app. Apple recommends asking in context and checking current settings because people can change their choices. Apple’s authorization guidance.
A previous permission request is not proof of current authorization. The following host-app helper only registers when notification authorization is already available; it never asks for permission. Put it in your app’s notification integration file and reuse it after your existing permission flow succeeds and whenever the app becomes active.
import UIKit
import UserNotifications
@MainActor
func registerForPushIfAuthorized() async {
let settings = await UNUserNotificationCenter.current().notificationSettings()
switch settings.authorizationStatus {
case .authorized, .provisional, .ephemeral:
UIApplication.shared.registerForRemoteNotifications()
default:
break
}
}For SwiftUI, merge the following into your existing app view. HomeView represents your existing app content. If your app already performs foreground registration, reuse that flow instead of adding another one. Configure ReceptionKit in your app initializer before these callbacks can run, as shown in Get started.
import SwiftUI
import ReceptionKit
struct RootView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
HomeView()
.task(id: scenePhase) {
if scenePhase == .active {
await registerForPushIfAuthorized()
}
}
}
}Provisional authorization can deliver quietly to Notification Center without a banner or sound. Check the device’s actual settings during verification. Do not treat quiet delivery as a failed APNs request. Apple’s provisional authorization behavior.
Forward the device token
Reception.shared.setPushToken
Add this call inside your existing app delegate callback. Preserve any forwarding to other notification services. Import UIKit and ReceptionKit in that file; the callback and Reception API run on the main actor.
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken token: Data
) {
Reception.shared.setPushToken(token)
// Keep your app's other token handling here.
}Forward every APNs callback, including token changes. Request a fresh token from the system on launch instead of treating an old host-cached token as authoritative. Keep or implement your app’s application(_:didFailToRegisterForRemoteNotificationsWithError:) handler to report registration failures and retry later. Apple’s token lifecycle guidance.
Reception.shared.setPushToken(_ token: Data, environment: Reception.PushEnvironment = .automatic) stages the hexadecimal token and resolved environment locally. Calls before Reception.configure(appId:), empty tokens, and unresolved environments are ignored. The method returns no delivery or synchronization result.
For a registered session that has started support, forwarding promptly starts a device update and unread refresh. Otherwise, the token is sent in a device update after registration on the first valid send attempt. Opening an unused empty chat or forwarding a token alone does not register a Reception device. After Reception.shared.logout() clears the staged token, use your registration flow to forward it again. See Login and logout.
Reception.PushEnvironment
| Value | Resolution |
|---|---|
.automatic | Default. Reads aps-environment from the embedded provisioning profile. development selects sandbox; production selects production. No embedded profile selects production. An unreadable profile or missing/unrecognized entitlement causes the token to be ignored. Simulator tokens are ignored. |
.sandbox | Explicit APNs sandbox. Use only for a known development token. |
.production | Explicit APNs production. Use for a known production token, including TestFlight and App Store builds. |
If detection is unavailable for your host, replace the forwarding call in the delegate with an explicit environment only after checking the signing configuration:
Reception.shared.setPushToken(token, environment: .sandbox)Use .production for the corresponding production token. Do not choose an environment from #if DEBUG: build flags do not establish how the app was provisioned. Do not force an override to turn a simulator test into evidence of real-device delivery.
Handle notifications
Reception.shared.handlePushNotification
Reception.shared.handlePushNotification(userInfo: [AnyHashable: Any], openChat: Bool = true) -> Bool is a main-actor method. It returns true when reception.conversationId is a nonempty string, and false for an unrelated or malformed payload.
A true result only means the payload was recognized. It does not validate the conversation against the server, confirm a refresh, or select a historical conversation by that ID. Reception opens the current session’s chat and requests an unread refresh. An old notification after logout cannot restore the previous session.
Use openChat: false for foreground receipt. For taps that open the SDK sheet, keep the default true, including after a cold start. For your own Support tab or navigation destination, pass false and open that destination when the method returns true. See Custom navigation.
Preserve delegate ownership
Merge the next two methods into the object that already owns UNUserNotificationCenterDelegate. Import UserNotifications and ReceptionKit there. Keep a single notification-center delegate and preserve unrelated notification handlers, custom actions, analytics, and your existing presentation policy. Do not implement both async and completion-handler variants of the same callback. With completion handlers, call each completion exactly once on every path.
If you have no notification delegate, make your existing app delegate conform to UNUserNotificationCenterDelegate and assign UNUserNotificationCenter.current().delegate = self during application(_:didFinishLaunchingWithOptions:). If the SwiftUI app has no app delegate either, connect one with @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate in the App type. Reuse an existing adaptor when present. Retain a separate delegate object for the application’s lifetime if you use one.
Foreground receipt
This example belongs in the existing notification delegate. It suppresses presentation while Support chat is visible and otherwise requests a banner and sound. The final return is an example fallback: preserve your existing policy for other notifications there.
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
let payload = notification.request.content.userInfo["reception"] as? [String: Any]
if let conversationId = payload?["conversationId"] as? String {
let options: UNNotificationPresentationOptions? = await MainActor.run {
guard Reception.shared.handlePushNotification(
userInfo: ["reception": ["conversationId": conversationId]],
openChat: false
) else { return nil }
return Reception.shared.isChatOpen ? [] : [.banner, .sound]
}
if let options { return options }
}
// Preserve your existing presentation policy for other notifications.
return [.banner, .sound]
}Notification taps
Add this alongside foreground routing in the same delegate. Keep existing custom-action routing when your app uses notification actions; this example handles the ordinary Support tap.
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
let payload = response.notification.request.content.userInfo["reception"] as? [String: Any]
if let conversationId = payload?["conversationId"] as? String {
let handled = await MainActor.run {
Reception.shared.handlePushNotification(
userInfo: ["reception": ["conversationId": conversationId]]
)
}
if handled { return }
}
// Run your existing tap handler for other notifications here.
}Both examples extract a String before crossing to the main actor and reconstruct the minimal recognized payload there. Do not capture the original [AnyHashable: Any], UNNotification, or UNNotificationResponse inside MainActor.run or a main-actor task: the raw payload contains non-Sendable values.
App badge and unread refresh
Reception.shared.updatesAppBadge defaults to true. ReceptionKit mirrors its unread count to the app icon. When that count reaches zero, it removes delivered notifications whose payload contains reception. To let your app manage both behaviors, set the following during app initialization on the main actor:
Reception.shared.updatesAppBadge = falseThis disables SDK badge synchronization and notification removal. Registration includes the preference; active support sessions synchronize changes asynchronously and retry after failures and on foreground. After the server saves false, new support pushes omit aps.badge entirely while keeping their alert and configured sound. Offline, the last successfully saved preference remains in effect. Already prepared or delivered notifications are not changed. Set the flag on each app launch; ReceptionKit does not combine host feature counts or clear an existing badge when you change it.
After support usage starts, ReceptionKit refreshes unread state on foreground. Automatic checks pause while push is configured and notification permission is available, and can resume if permission is revoked. Checks stop in the background. See Unread messages.
Configure APNs in the dashboard
As an owner, open Settings → Apps → your app → Apple Push Notifications. For a new connection, use the key guide and select I have the key. For an existing connection, select Edit connection.
Obtain an APNs signing key from Apple Developer → Certificates, Identifiers & Profiles → Keys. Upload the .p8 private key through the Reception dashboard; do not bundle it in the app. Apple provides the key file and its Key ID for token authentication. Apple’s APNs key documentation.
| Dashboard field | Value |
|---|---|
| Bundle ID | The signed app’s bundle identifier, for example com.example.app. Used as the APNs topic. |
| Key ID | The key’s 10-character identifier, uppercase letters and numbers. |
| Team ID | Your Apple Developer team’s 10-character identifier, uppercase letters and numbers. |
| Private key (.p8) | Upload the APNs signing key, at most 16 KB. Reception stores the key encrypted. |
Select Connect, or Save changes when editing. Replace key (.p8) is optional when a key is already saved; leaving it unchanged retains the saved key. Disconnect removes the key after confirmation while retaining identifiers and notification content settings. Saving settings does not send a notification.
Match key and device environments
There is no dashboard environment selector. Reception uses the environment reported by each device. Reception stores one APNs key per app.
Create the key with the environment Sandbox & Production. Development-provisioned builds use Sandbox; distribution-provisioned builds, including TestFlight and App Store builds, use Production. A key limited to Sandbox or to Production only reaches builds in that environment. Apple’s key options.
Configured means credentials are saved. Onboarding does not test delivery. In app settings, choose Send test and confirm arrival on a real device, including after correcting a connection. A reported rejection may remain until a delivery succeeds.
Offer pushes
Offer pushes are off by default. As an owner, open Settings → Review & Offer CTAs, find your app, and turn on Send a push notification under Paywalls & discount offers. The app still needs the APNs setup above.
The preview uses the card message, or the chosen custom button label when the message is empty. With neither, it uses “See your discount” in the customer's language. Your notification templates, Show message, and Sound settings also apply. Turning off Show message replaces the preview with a generic prompt.
Enable promotional pushes only after customers explicitly opt in through your app, and provide an in-app opt-out, as required by Apple's guideline 4.5.4.
Customize notification content
In app settings, open Apple Push Notifications → Notification content. Choose a preview language and select Customize to edit Title, Message, and Sender name. The preview uses a two-photo example and does not send a push.
Show message
Enabled by default. Reply text becomes the preview; text with photos gets a photo marker, and photos without text get a localized photo summary. Turning this off replaces the reply placeholder with a localized prompt to read the reply, including inside custom templates. The actual chat message is unchanged. See Privacy and data.
Sound
Enabled by default. When enabled, the payload includes sound: "default"; when disabled, it omits that field. Actual presentation also depends on iOS settings and your foreground delegate policy.
Title and Message
Overrides apply to the selected language only, with a maximum of 120 characters per template. Spaces count toward the limit. Blank or default-equivalent fields use built-in text. The Insert menus provide these substitutions:
| Variable | Meaning |
|---|---|
{message} | Reply preview or localized hidden-message prompt, depending on Show message. |
{agent} | Global Sender name, or the localized Support default. It is not the replying agent’s profile name. |
{app} | The app name saved in Reception. |
Rendered titles are capped at 100 characters and bodies at 500 characters. Empty rendered titles fall back to the app name; empty bodies fall back to the message placeholder. Reset text to default clears the selected language’s title and message overrides. Save commits edits; Cancel discards them.
Sender name
One optional name shared by all languages, trimmed and limited to 60 characters. Clearing it restores the localized default. Setting it changes the default title to include the sender; custom titles only include it when they use {agent}.
Notification language
Delivery uses the device’s last successfully reported app language, matched to a supported language or English. Device locale is used only for older devices without an app-language report. Custom templates are not automatically translated. Changing Preview language edits/previews translations; it does not change a customer’s language. See Language for all supported codes and host-app language synchronization.
Payload shape
The backend sends the following shape. This JSON is a synthetic example for understanding delegate routing, not proof of delivery. Put equivalent test data in your own notification test fixture if needed; the backend generates real payloads automatically.
{
"aps": {
"alert": {
"title": "New message from support",
"body": "Your reply is ready."
},
"badge": 1,
"sound": "default",
"thread-id": "example-conversation-id"
},
"reception": {
"conversationId": "example-conversation-id"
}
}aps.thread-id groups notifications for the conversation. reception.conversationId is the SDK recognition marker. Sound is optional. When the device’s last saved updatesAppBadge preference is true (the default for older clients), the badge carries the conversation’s unread-for-user count. With false, aps.badge is omitted, including when the count is zero.
Verify on a real device
Install a correctly signed build on your iPhone. Confirm the bundle ID, push entitlement, and signing environment.
Grant permission through your app’s existing intentional flow. Return to the app so the foreground check registers with APNs. Confirm the delegate forwards a nonempty token after ReceptionKit configuration; do not publish the token in logs or screenshots.
Send a support message from the phone. This registers the Reception device, uploads its staged push information, and creates a conversation.
Save the matching APNs credentials. In app settings, open Apple Push Notifications → Send test, choose the Device, and select Send test. Labels include Sandbox or Production. The selector lists up to ten eligible devices ordered by recent chat activity; use your test device recently if it is missing.
Observe the notification on the phone. A successful test action means APNs accepted the request, not that the notification was displayed. Tests use saved content settings and an existing conversation; they do not create a chat message.
Background the app and send a real dashboard reply. Confirm arrival, tap the notification, and verify chat opens and displays the reply. Repeat a tap when the app is not running to check launch routing.
Repeat with the app foregrounded and chat closed, then with chat open. Check unread refresh and the intended banner/sound policy. Confirm unrelated notifications retain their behavior.
Test development and distribution builds separately with compatible credentials. Also test denied permission, re-enabling permission in Settings, and forwarding again after logout.
Troubleshooting
| Symptom | Check |
|---|---|
| No APNs registration callback | Check push capability, signed entitlements, network access, and didFailToRegisterForRemoteNotificationsWithError. Recheck current permission in your registration helper. |
| No eligible device in Send test | Use a real iPhone, forward its token after configuration, and send a support message. An unused chat does not register a device. Refresh app settings after synchronization. |
| Token is ignored | Check for an empty token, calls before configuration, simulator automatic mode, and unreadable/missing profile entitlements. Forward again after logout. |
| BadDeviceToken or environment rejection | Compare Sandbox/Production with provisioning. Correct explicit overrides and ensure the signing key permits that environment. |
| Topic or credential rejection | Check Bundle ID against the signed app, Key ID against the uploaded key, Team ID, key scope, and revocation status in Apple Developer. |
| Configured but nothing arrives | Confirm the device has a conversation and token, credentials match its environment, and notification settings permit the expected presentation. Check Notification Center for quiet delivery. |
| APNs records a rejection | Inspect Push issue in device details. Check the reason against your Apple credentials, bundle ID, and device environment, then send a test or reply after correcting it. Contact Reception if it persists. |
| No rejection shown despite failure | Confirm credentials and device setup, then test delivery. An empty Push issue field does not prove success. |
| Tap does not open chat | Configure before callbacks and preserve delegate routing. Use openChat: true for the SDK sheet, or false and your own navigation for a host-owned chat. |
| Swift concurrency diagnostics | Extract the conversation ID before the actor hop. Rebuild the minimal dictionary on the main actor instead of transferring the original notification or payload. |
| Unexpected badge changes | Review Reception.shared.updatesAppBadge and your delegate options. The server omits the APNs badge field only after the preference is successfully synchronized; offline or already prepared pushes can still use the previous value. |
| Permission is denied | Respect the decision. Use your existing settings flow and recheck on foreground; do not introduce repeated permission prompts. Chat still works. |
For broader networking and integration issues, see Troubleshooting and the SDK reference.