Elixir client for the Apple Push Notification service (APNs).
Provides a complete interface for sending push notifications to iOS, macOS, watchOS, and tvOS devices.
Features
- HTTP/2 connection support (required by APNs)
- JWT-based authentication with P8 keys
- Automatic token caching with expiration buffer
- Rich notification support (images, actions, custom UI)
- VoIP push support
- Comprehensive error handling with device token validation
Quick Start
# Configure in config/config.exs
config :apple_push_notifications,
team_id: System.get_env("APNS_TEAM_ID"),
key_id: System.get_env("APNS_KEY_ID"),
bundle_id: System.get_env("APNS_BUNDLE_ID"),
private_key: System.get_env("APNS_PRIVATE_KEY"),
sandbox: true # Set to false for production
# Send a simple push notification
ApplePushNotifications.push(
"a1b2c3d4e5f6789...",
alert: "Hello World!",
badge: 1
)
# Send a rich notification with custom payload
ApplePushNotifications.push(
"a1b2c3d4e5f6789...",
alert: %{title: "Breaking News", body: "Something happened!"},
sound: "news_alert.caf",
custom: %{article_id: "12345", category: "sports"}
)Configuration
Required configuration:
team_id: Your Apple Developer Team ID (10 characters)key_id: Your APNs Auth Key ID (10 characters)bundle_id: Your app's bundle identifier (e.g., "com.example.myapp")private_key: The P8 private key content (or useprivate_key_path)sandbox:truefor development,falsefor production
Optional configuration:
base_url: Override the APNs endpoint (default: api.push.apple.com or api.sandbox.push.apple.com)token_ttl_seconds: JWT expiration time (default: 1200 seconds / 20 minutes)
Per-Call Options
Every function accepts per-call opts that override the application config:
ApplePushNotifications.push(
device_token,
alert: "Hello",
bundle_id: "com.example.differentapp" # Override bundle_id for this call
)Error Handling
APNs returns specific error codes for different failure scenarios:
BadDeviceToken: Invalid or malformed device tokenUnregistered: Device token is no longer valid (app uninstalled or token expired)PayloadTooLarge: Notification payload exceeds 4KB (4096 bytes)TooManyRequests: Rate limit exceededInternalServerError: APNs server error, retry later
Use ApplePushNotifications.Error.invalid_token?/1 to check if an error
indicates the device token should be removed from your database.
Background Notifications
Send silent background notifications:
ApplePushNotifications.background_push(
device_token,
content_available: true,
custom: %{sync_data: true}
)VoIP Push Notifications
Send VoIP pushes for CallKit integration:
ApplePushNotifications.voip_push(
device_token,
handle: "+1234567890",
display_name: "Incoming Call"
)Interruption Levels (iOS 15+)
Control notification urgency:
ApplePushNotifications.push(
device_token,
alert: "Important!",
interruption_level: :time_sensitive, # :passive, :active, :time_sensitive, :critical
relevance_score: 0.75
)
Summary
Functions
Send a silent background notification.
Check if an error indicates an invalid/unregistered device token.
Send a push notification to a device.
Send a notification to multiple devices.
Return a cached APNs access token (after JWT generation).
Validate a device token format.
Send a VoIP push notification for CallKit.
Types
Functions
Send a silent background notification.
Background notifications wake up the app to perform work without alerting the user.
Parameters
device_token: The device tokenopts:content_available: Must be true (default)custom: Custom data for background processing- Per-call config overrides
Examples
ApplePushNotifications.background_push(token,
custom: %{fetch_new_data: true, endpoint: "/api/sync"}
)
Check if an error indicates an invalid/unregistered device token.
Returns true if the error is one of:
BadDeviceToken: Token format is invalidUnregistered: Token is no longer valid (app uninstalled, token rotated)
When this returns true, you should remove the device token from your database.
Examples
case ApplePushNotifications.push(token, alert: "Hello") do
{:ok, _} -> :ok
{:error, error} ->
if ApplePushNotifications.invalid_token_error?(error) do
# Remove token from database
User.remove_device_token(user_id, token)
end
end
Send a push notification to a device.
Parameters
device_token: The 64-character hex device tokenopts:alert: String alert message or map withtitle/body/subtitlebadge: Integer badge count (nil to leave unchanged)sound: Sound file name or "default"custom: Map of custom payload datainterruption_level: One of:passive,:active,:time_sensitive,:criticalrelevance_score: Float between 0 and 1 (iOS 15+)- Per-call config overrides (
team_id,key_id,bundle_id, etc.)
Examples
# Simple text notification
ApplePushNotifications.push(token, alert: "Hello!")
# Rich notification with badge
ApplePushNotifications.push(token,
alert: %{title: "News", body: "New article available"},
badge: 5,
sound: "news.caf"
)
# With custom data
ApplePushNotifications.push(token,
alert: "Update available",
custom: %{version: "2.0", force_update: false}
)
Send a notification to multiple devices.
Note: Unlike Firebase Cloud Messaging, APNs does not support multicast in a single request. This function iterates over device tokens and sends individual requests.
Parameters
device_tokens: List of device tokensopts: Same options aspush/2
Returns
{:ok, results}: Map of device_token to:okor{:error, reason}{:error, reason}: Configuration error before sending
Examples
ApplePushNotifications.push_many([token1, token2, token3],
alert: "Broadcast message",
badge: 1
)
Return a cached APNs access token (after JWT generation).
Validate a device token format.
Device tokens should be exactly 64 hexadecimal characters.
Examples
ApplePushNotifications.valid_token?("a1b2c3d4e5f6...") # true/false
Send a VoIP push notification for CallKit.
VoIP pushes use a different topic (bundle ID + ".voip").
Parameters
device_token: The VoIP device tokenopts:handle: Phone number or handle for the calldisplay_name: Caller display namecustom: Additional CallKit data- Per-call config overrides (bundle_id will have ".voip" appended)
Examples
ApplePushNotifications.voip_push(token,
handle: "+1-555-123-4567",
display_name: "John Doe",
custom: %{uuid: "...", session_id: "..."}
)