Push Notifications
Status: Fully implemented in v3.0.0 (core push system) and v3.1.0 (per-app config + Console UI).
Overview
Keymaster brokers push notifications for your app. Your backend sends notifications via Keymaster's API; Keymaster delivers to FCM (Android, iOS, Web) and Web Push (Safari). Your app never touches Firebase or APNs directly.
Architecture
Your Backend
↓ POST /push/send (client_credentials JWT)
Keymaster Push Service
↓ FCM / Web Push
User's Device
↓ delivery receipt
Keymaster → Your Webhook URL
Setup
1. Enable Notifications
Push notifications are enabled per-app. You can enable via the Console UI (App Detail → Notifications tab → Enable) or programmatically:
PATCH /push/config/{app_id}
Authorization: Bearer {admin_jwt}
{
"enabled": true,
"webhook_url": "https://yourapp.com/hooks/push-receipts",
"daily_quota": 10000
}
On first enable, Keymaster auto-provisions: - A VAPID keypair for Web Push (Safari) - A webhook secret (auto-generated, returned in the response)
To check current push status and device count:
GET /push/config/{app_id}
Authorization: Bearer {admin_jwt}
→ {
"enabled": true,
"webhook_url": "https://yourapp.com/hooks/push-receipts",
"webhook_secret": "whsec_...",
"daily_quota": 10000,
"device_count": 42,
"notifications_today": 128
}
To list registered devices for an app:
GET /push/devices/{app_id}
Authorization: Bearer {admin_jwt}
→ {
"devices": [
{
"device_id": "uuid",
"user_id": "uuid",
"platform": "ios",
"last_seen_at": "2026-03-19T08:00:00Z"
}
],
"total": 42
}
2. Register Devices (Client-Side)
Prerequisite: The app must have push notifications enabled (notifications.enabled in config). Device registration will return 403 if push is not enabled for the app.
After the user authenticates in your app, request push permission and register the token:
// Web (using Firebase)
import { getMessaging, getToken } from 'firebase/messaging';
const messaging = getMessaging();
const fcmToken = await getToken(messaging, { vapidKey: 'your-vapid-key' });
// Register with Keymaster
await fetch('https://keymaster.cloud-monitor.com/push/devices/register', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userAccessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
app_id: 'your-app-id',
platform: 'web',
push_token: fcmToken,
}),
});
// iOS (after getting APNs token via Firebase)
func registerDevice(fcmToken: String, accessToken: String) {
var request = URLRequest(url: URL(string: "https://keymaster.cloud-monitor.com/push/devices/register")!)
request.httpMethod = "POST"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"app_id": "your-app-id",
"platform": "ios",
"push_token": fcmToken,
])
URLSession.shared.dataTask(with: request).resume()
}
3. Send Notifications (Server-Side)
Authenticate via client credentials, then call the push API:
from keymaster_client import KeymasterClient # see server-to-server.md
km = KeymasterClient(
base_url="https://keymaster.cloud-monitor.com",
client_id="your-app-id",
client_secret="your-secret",
)
# Single notification
km.send_push(
user_id="user-uuid",
title="Your shift starts in 30 min",
body="Open GymOps to view details.",
data={"route": "/schedule"},
)
API Endpoints
Register Device
POST /push/devices/register
Authorization: Bearer {user_jwt}
{
"app_id": "uuid",
"platform": "web" | "ios" | "android",
"push_token": "fcm-token-or-web-push-subscription"
}
→ { "device_id": "uuid" }
Rate limited: 10 registrations per user per hour.
Unregister Device
App-initiated (server-side):
DELETE /push/devices/{device_id}
Authorization: Bearer {client_credentials_jwt scope=push:send}
User-initiated (Account page):
DELETE /push/devices/{device_id}
Authorization: Bearer {user_jwt}
Send (Single)
POST /push/send
Authorization: Bearer {client_credentials_jwt scope=push:send}
{
"user_id": "uuid",
"title": "Notification title",
"body": "Notification body text",
"data": { "route": "/some-page" },
"ttl": 3600
}
→ { "notification_id": "uuid" }
Send Bulk (Same Message to Many)
POST /push/send/bulk
Authorization: Bearer {client_credentials_jwt scope=push:send}
{
"user_ids": ["uuid1", "uuid2", ...],
"title": "Gym closes early today",
"body": "Closing at 6pm",
"data": {}
}
→ { "notification_ids": ["uuid1", "uuid2", ...] }
Max user_ids: 500 (configurable via PUSH_MAX_BATCH_SIZE).
Send Batch (Different Messages)
POST /push/send/batch
Authorization: Bearer {client_credentials_jwt scope=push:send}
{
"notifications": [
{ "user_id": "uuid1", "title": "Shift approved", "body": "..." },
{ "user_id": "uuid2", "title": "Time off denied", "body": "..." }
]
}
Max notifications: 500.
Delivery Receipts
After FCM/APNs responds, Keymaster POSTs to your app's webhook URL:
POST https://yourapp.com/hooks/push-receipts
X-Keymaster-Signature: sha256=...
Content-Type: application/json
{
"notification_id": "uuid",
"user_id": "uuid",
"device_id": "uuid",
"app_id": "uuid",
"status": "delivered" | "failed" | "invalid_token",
"error": null,
"timestamp": "2026-03-17T12:00:00Z"
}
On invalid_token, Keymaster auto-removes the device registration.
Quotas
- Default: 10,000 notifications per day per app
- Configurable per-app via
PATCH /push/config/{app_id}(daily_quotafield) - Current usage visible via
GET /push/config/{app_id}(notifications_todayfield) - Counter resets at midnight UTC
- 429 response with
Retry-Afterwhen exceeded
User Device Management
Users can view and manage their registered devices from the Account page:
GET /account/devices
Authorization: Bearer {account_jwt}
Returns all devices grouped by app, with platform and last_seen_at. Users can revoke individual devices.
Validation Rules
datafield: max 4KB (FCM limit)title: required, max 256 charactersbody: required, max 4096 characters- Send endpoints verify the target
user_idis enrolled in the calling app - Push tokens are encrypted at rest in the database