Python script to send push notifications to iOS devices using Apple's HTTP/2 APNs with .p8 certificate authentication.
- Apple Developer Account with push notification capabilities
- .p8 Authentication Key from Apple Developer Console
- iOS app configured for push notifications
- Device token from your iOS app
pip install -r requirements.txt
Dependencies:
PyJWT
- JWT token generationcryptography
- ES256 signinghttpx[http2]
- HTTP/2 requests to APNs (required by Apple)python-dotenv
- Environment variable loading
The script now automatically detects the correct environment (Sandbox vs Production)! If you get a BadDeviceToken
error, it will automatically retry with the opposite environment and tell you which one works.
Team ID:
- Apple Developer Console → Membership → Team ID (10 characters)
Key ID & .p8 File:
- Apple Developer Console → Keys → Create Key
- Enable "Apple Push Notifications service (APNs)"
- Download the .p8 file (save it securely, can't re-download)
- Note the Key ID (10 characters)
Bundle ID:
- Your app's identifier (e.g.,
com.yourcompany.yourapp
)
Device Token:
- Get from your iOS app using:
// In your iOS app
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Device Token: \(token)")
}
Create a .env
file by copying the example:
cp config.env.example .env
Edit .env
with your actual values:
TEAM_ID=ABC123DEFG
KEY_ID=XYZ987WXYZ
KEY_FILE_PATH=AuthKey_XYZ987WXYZ.p8
BUNDLE_ID=com.yourcompany.yourapp
DEVICE_TOKEN=your64characterdevicetokenhere
Note: The .env
file is automatically ignored by git for security. No need to specify sandbox vs production - the script auto-detects the correct environment!
python push_notification.py
from push_notification import APNSender
# Initialize
sender = APNSender(
team_id="ABC123DEFG",
key_id="XYZ987WXYZ",
key_file_path="AuthKey_XYZ987WXYZ.p8",
bundle_id="com.yourcompany.yourapp",
use_sandbox=True # False for production
)
# Send notification
result = sender.send_notification(
device_token="your_device_token_here",
title="Hello World",
body="This is a test notification",
badge=5,
sound="default",
custom_data={
"user_id": 123,
"action": "open_chat"
}
)
if result["success"]:
print("✅ Sent successfully")
else:
print(f"❌ Failed: {result['error']}")
# Silent notification (no alert)
result = sender.send_notification(
device_token="...",
title="",
body="",
sound="", # Empty string for silent
custom_data={
"content-available": 1, # Background refresh
"data": "your_data_here"
}
)
# Custom sound
result = sender.send_notification(
device_token="...",
title="Custom Alert",
body="With custom sound",
sound="custom_sound.wav" # Must be in app bundle
)
The script now automatically detects the correct environment! Just run it and it will:
- Start with sandbox environment (most common for development)
- If it gets
BadDeviceToken
, automatically try production environment - Tell you which environment worked
- Remember the working environment for future calls
# The script will automatically figure out sandbox vs production
result = sender.send_notification(
device_token="...",
title="Test",
body="Auto-detection!"
)
# Disable auto-retry if you want manual control
result = sender.send_notification(
device_token="...",
title="Test",
body="Manual mode",
auto_retry=False
)
# Force sandbox
sender = APNSender(..., use_sandbox=True)
# Force production
sender = APNSender(..., use_sandbox=False)
# Override bundle ID for specific notification
result = sender.send_notification(
device_token="...",
title="Multi-app notification",
body="Message",
topic="com.yourcompany.anotherapp" # Different app
)
Code | Meaning |
---|---|
200 | Success |
400 | Bad request (malformed JSON, missing fields) |
403 | Invalid certificate or topic |
405 | Method not allowed |
410 | Device token inactive (user uninstalled app) |
413 | Payload too large (max 4KB) |
429 | Too many requests |
500 | Internal server error |
"Invalid JWT token":
- Check team_id and key_id are correct
- Verify .p8 file path and contents
- Ensure key has APNs permission
"BadDeviceToken":
- Device token must be 64 hex characters
- Check sandbox vs production mismatch
- Token may be expired/invalid
"TopicDisallowed":
- Bundle ID doesn't match certificate
- App not configured for push notifications
"PayloadTooLarge":
- Max payload size is 4KB
- Reduce title/body/custom_data size
├── push_notification.py # Main script
├── requirements.txt # Dependencies
├── README.md # This file
├── config.env.example # Environment variables template
├── .env # Your actual environment variables (not tracked)
├── .gitignore # Git ignore file
└── AuthKey_XXXXXXXXXX.p8 # Your .p8 certificate (not tracked)
- Keep your .p8 file secure and private
- Don't commit .p8 files to version control
- Don't commit .env files to version control
- JWT tokens expire after 1 hour (automatically refreshed)
- Use environment variables for sensitive data in production
- The
.gitignore
file automatically excludes.env
and.p8
files from git