Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

APNs

The APNs transport talks to Apple Push Notification service directly — HTTP/2 with token-based (.p8) authentication. No FCM/Google dependency for Apple devices. It handles iOS and macOS registrations. It ships in the Shiny.Extensions.Push package (namespace Shiny.Extensions.Push.Apns) — no separate package to install; just add using Shiny.Extensions.Push.Apns; to reach AddApns.

Requires a paid Apple Developer Program membership.

  1. Create an APNs auth key. Go to Certificates, Identifiers & Profiles → Keys, click +, name the key, tick Apple Push Notifications service (APNs) and register it. Download the AuthKey_XXXXXXXXXX.p8 file, which can only be downloaded once, and note the Key ID.
  2. Find your Team ID under Membership details.
  3. Check the App ID. Under Identifiers, your App ID’s bundle id is BundleId, and it must have the Push Notifications capability enabled.

One .p8 key works for every app on the team, in both sandbox and production. The app side (entitlements, background mode, reporting the token) is covered in the client Platform Setup → Apple.

You need the .p8 auth key, its Key ID, your Team ID, and the app’s bundle id.

push.AddApns(o =>
{
o.TeamId = "ABCDE12345"; // 10-char Apple Team ID (JWT "iss")
o.KeyId = "KEY1234567"; // 10-char Key ID of the .p8 (JWT "kid")
o.BundleId = "com.example.app"; // default apns-topic
o.PrivateKeyPath = "AuthKey_KEY1234567.p8"; // or o.PrivateKey = "<PEM contents>"
// o.ForceEnvironment = PushEnvironment.Sandbox; // default: honour each registration
});

The provider builds and caches its ES256 provider JWT (refreshed ~every 50 minutes — Apple rejects tokens regenerated too frequently) and reuses a single pooled HTTP/2 connection across all sends.

APNs tokens are environment-specific — a token registered against the sandbox (debug/TestFlight builds) is not valid in production and vice-versa. Set DeviceRegistration.Environment per device:

await pushManager.RegisterDevice(new DeviceRegistration
{
DeviceToken = "<token>",
Platform = DevicePlatform.iOS,
Environment = PushEnvironment.Sandbox
});

The provider routes each push to the correct host based on the registration. Use ApnsOptions.ForceEnvironment only if you want to pin every push to one environment.

ApplePushOptions on the notification maps to the aps dictionary and APNs headers:

new PushNotification
{
Title = "Title",
Apple = new ApplePushOptions
{
Subtitle = "Subtitle",
Category = "MESSAGE_CATEGORY", // actionable notifications
ThreadId = "thread-1", // grouping
MutableContent = true, // Notification Service Extension
ContentAvailable = false, // true => silent/background push
// TopicOverride / PushTypeOverride for VoIP / complication topics
}
};

Common cross-cutting fields also map to APNs: Badge, Sound, CollapseId (apns-collapse-id), TimeToLive (apns-expiration), and Priority (10/5; forced to 5 for background pushes).

The provider normalizes APNs responses to PushDeliveryStatus:

APNs result Status Manager action
410 Unregistered TokenExpired token pruned
BadDeviceToken / DeviceTokenNotForTopic InvalidToken token pruned
429 TooManyRequests RateLimited surfaced (no auto-retry)
ExpiredProviderToken / InvalidProviderToken Error JWT cache invalidated

Register one keyed provider per bundle id — see Sending → Multiple apps. Each key gets its own options, .p8 key, and JWT cache.