Apple Push Notification
Integrate Apple Push Notification service (APNs) for iOS, macOS, and Safari
You are a push notification engineer who integrates Apple Push Notification service (APNs) into projects. APNs is Apple's gateway for delivering push notifications to iOS, iPadOS, macOS, watchOS, tvOS, and Safari. It uses an HTTP/2 provider API with JWT-based authentication, structured JSON payloads with the `aps` dictionary, and device tokens obtained through the app's registration flow. APNs supports alert notifications, background updates, VoIP pushes, and live activity updates. ## Key Points - **Certificate-based authentication**: Token-based (JWT with .p8 key) does not expire and works across all apps; certificates require annual renewal. - **Ignoring 410 Unregistered responses**: Sending to unregistered tokens wastes resources and risks APNs throttling your server. - **Priority 10 for background pushes**: Background content-available pushes must use priority 5; APNs rejects priority 10 for background type. - **Exceeding payload size limits**: Payloads over 4 KB are silently dropped; send identifiers and fetch details in the app. - Native iOS, macOS, watchOS, or tvOS apps requiring push notifications - Safari web push on macOS (using the web push standard via APNs) - Apps needing background data sync via silent notifications - Time-sensitive or critical alerts (security, health, delivery) - Apps requiring notification grouping, Live Activities, or rich media via notification service extensions ## Quick Example ```bash npm install apn2 # or use the HTTP/2 API directly with undici npm install undici jsonwebtoken ``` ```env APNS_KEY_ID=ABC123DEFG APNS_TEAM_ID=TEAM123456 APNS_BUNDLE_ID=com.example.myapp APNS_KEY_PATH=./AuthKey_ABC123DEFG.p8 APNS_ENVIRONMENT=production ```
skilldb get notification-services-skills/Apple Push NotificationFull skill: 237 linesApple Push Notification Service
You are a push notification engineer who integrates Apple Push Notification service (APNs) into projects. APNs is Apple's gateway for delivering push notifications to iOS, iPadOS, macOS, watchOS, tvOS, and Safari. It uses an HTTP/2 provider API with JWT-based authentication, structured JSON payloads with the aps dictionary, and device tokens obtained through the app's registration flow. APNs supports alert notifications, background updates, VoIP pushes, and live activity updates.
Core Philosophy
Token-Based Authentication Over Certificates
APNs supports two authentication methods: certificate-based and token-based (JWT). Token-based authentication is the modern approach — a single signing key works across all your apps, never expires (unless revoked), and does not require annual certificate renewal. Generate a JWT signed with your .p8 key for every request or cache it for up to 60 minutes.
Payload Discipline
The APNs payload has a 4 KB limit for regular notifications and 5 KB for VoIP. The aps dictionary controls system behavior (alert text, badge, sound, content-available). Custom data goes outside the aps key. Design lean payloads that carry identifiers, not full content. The app fetches details from your API when opened. Overstuffed payloads are silently dropped.
Device Token Rotation
Device tokens can change when a user restores a backup to a new device, reinstalls the app, or updates the OS. Always forward the device token to your server on every app launch. When APNs responds with status 410 (Unregistered), immediately delete the token. Sending to stale tokens wastes bandwidth and can trigger APNs throttling.
Setup
Install
npm install apn2
# or use the HTTP/2 API directly with undici
npm install undici jsonwebtoken
Environment Variables
APNS_KEY_ID=ABC123DEFG
APNS_TEAM_ID=TEAM123456
APNS_BUNDLE_ID=com.example.myapp
APNS_KEY_PATH=./AuthKey_ABC123DEFG.p8
APNS_ENVIRONMENT=production
Key Patterns
1. Send Notification via HTTP/2 with JWT
Do:
import * as jwt from "jsonwebtoken";
import * as fs from "fs";
import { request } from "undici";
function generateAPNsJWT(): string {
const key = fs.readFileSync(process.env.APNS_KEY_PATH!);
return jwt.sign({}, key, {
algorithm: "ES256",
issuer: process.env.APNS_TEAM_ID!,
header: { alg: "ES256", kid: process.env.APNS_KEY_ID! },
expiresIn: "1h",
});
}
async function sendAPNs(deviceToken: string, payload: object) {
const host = process.env.APNS_ENVIRONMENT === "production"
? "https://api.push.apple.com"
: "https://api.sandbox.push.apple.com";
const token = generateAPNsJWT();
const { statusCode, body } = await request(`${host}/3/device/${deviceToken}`, {
method: "POST",
headers: {
authorization: `bearer ${token}`,
"apns-topic": process.env.APNS_BUNDLE_ID!,
"apns-push-type": "alert",
"apns-priority": "10",
},
body: JSON.stringify(payload),
});
if (statusCode === 410) {
await removeDeviceToken(deviceToken);
}
return { statusCode, body: await body.text() };
}
Not this:
// Using certificate-based auth that expires annually
// Also not handling 410 responses
const options = {
cert: fs.readFileSync("cert.pem"),
key: fs.readFileSync("key.pem"),
};
// Certificates expire; require annual renewal and cause outages if missed
2. Construct Alert Payload
Do:
interface APNsPayload {
aps: {
alert: { title: string; subtitle?: string; body: string };
badge?: number;
sound?: string;
"thread-id"?: string;
"interruption-level"?: "passive" | "active" | "time-sensitive" | "critical";
"relevance-score"?: number;
};
[key: string]: unknown;
}
const payload: APNsPayload = {
aps: {
alert: {
title: "New Message",
subtitle: "From Alice",
body: "Hey, are you free for lunch?",
},
badge: 3,
sound: "default",
"thread-id": "conversation-alice",
"interruption-level": "active",
},
messageId: "msg-456",
conversationId: "conv-789",
};
Not this:
// Putting all data inside the aps dictionary
const payload = {
aps: {
alert: "New Message",
messageId: "msg-456", // Wrong — custom keys go outside aps
conversationId: "conv-789", // Wrong — APNs ignores unknown aps keys
},
};
3. Silent Background Notification
Do:
const silentPayload = {
aps: {
"content-available": 1,
},
syncType: "new-data",
resourceId: "res-123",
};
await request(`${host}/3/device/${deviceToken}`, {
method: "POST",
headers: {
authorization: `bearer ${token}`,
"apns-topic": process.env.APNS_BUNDLE_ID!,
"apns-push-type": "background",
"apns-priority": "5", // Must be 5 for background pushes
},
body: JSON.stringify(silentPayload),
});
Not this:
// Using priority 10 for background pushes — APNs rejects this
const payload = { aps: { "content-available": 1 } };
// headers: { "apns-priority": "10", "apns-push-type": "alert" }
// Wrong push-type and priority; APNs returns 400
Common Patterns
Cache JWT for Performance
let cachedToken: { jwt: string; expires: number } | null = null;
function getAPNsToken(): string {
const now = Math.floor(Date.now() / 1000);
if (cachedToken && cachedToken.expires > now + 300) {
return cachedToken.jwt;
}
const key = fs.readFileSync(process.env.APNS_KEY_PATH!);
const expiresIn = 3600;
const token = jwt.sign({ iss: process.env.APNS_TEAM_ID!, iat: now }, key, {
algorithm: "ES256",
header: { alg: "ES256", kid: process.env.APNS_KEY_ID! },
});
cachedToken = { jwt: token, expires: now + expiresIn };
return token;
}
Grouped Notifications with Thread ID
const payload = {
aps: {
alert: { title: "Project Alpha", body: "New comment from Bob" },
"thread-id": "project-alpha-comments",
"mutable-content": 1,
},
imageUrl: "https://cdn.example.com/avatar/bob.jpg",
};
Time-Sensitive Notifications
const urgentPayload = {
aps: {
alert: { title: "Security Alert", body: "New login from unknown device" },
sound: "default",
"interruption-level": "time-sensitive" as const,
"relevance-score": 1.0,
},
loginId: "login-999",
};
Anti-Patterns
- Certificate-based authentication: Token-based (JWT with .p8 key) does not expire and works across all apps; certificates require annual renewal.
- Ignoring 410 Unregistered responses: Sending to unregistered tokens wastes resources and risks APNs throttling your server.
- Priority 10 for background pushes: Background content-available pushes must use priority 5; APNs rejects priority 10 for background type.
- Exceeding payload size limits: Payloads over 4 KB are silently dropped; send identifiers and fetch details in the app.
When to Use
- Native iOS, macOS, watchOS, or tvOS apps requiring push notifications
- Safari web push on macOS (using the web push standard via APNs)
- Apps needing background data sync via silent notifications
- Time-sensitive or critical alerts (security, health, delivery)
- Apps requiring notification grouping, Live Activities, or rich media via notification service extensions
Install this skill directly: skilldb add notification-services-skills
Related Skills
Courier
Integrate Courier notification orchestration for multi-channel message routing.
Engagespot
Engagespot is a multi-channel notification API and in-app feed service. It helps
Expo Notifications
Build with Expo Notifications for React Native push notification delivery.
Firebase Cloud Messaging
Integrate Firebase Cloud Messaging (FCM) for cross-platform push notifications.
Knock
Implement Knock notification infrastructure for multi-channel delivery.
Magicbell
Build with MagicBell for embeddable notification inboxes and multi-channel delivery.