Push

Turns Web Push (RFC 8030) sent by servers, and messages from your app's backend, into iOS system notifications (APNs). It suits servers that can only send Web Push (such as a mail server's JMAP push subscriptions), as well as your own backend pushing notifications to specific users.

Content is end-to-end encrypted: the sender encrypts with the device's public key per RFC 8291 (aes128gcm). CZL Connect only handles the ciphertext, which it places unchanged in the notification's custom key c for the app's notification extension to decrypt and display. CZL Connect cannot see notification content.

Use cases

  • A mail, calendar or other server that only supports Web Push needs to notify an iOS app about new items
  • Your backend pushes notifications per user (order status, reminders) to every device that user has
  • Several apps share one Apple push key without each running its own push service

Requirements

Push is available based on the app owner's plan. The quota is shared across all apps in the account and counts notifications successfully handed to the push platform each month (messages stashed while pending and failed sends are not counted):

PlanPushes per month
LiteNot supported
Plus50,000
Pro500,000
EnterpriseUnlimited

Once the quota is used up, Web Push delivery returns 429 (1009) and backend sends return quota_exceeded per message. Push URLs stay valid and resume next month or after an upgrade.

Set up a push credential

Push credentials belong to your account, and one can be shared by several of your apps:

  1. Apple Developer → Certificates, Identifiers & Profiles → Keys: create a key with Apple Push Notifications service (APNs) enabled, download the .p8 file (it can only be downloaded once) and note the Key ID
  2. Your Team ID is on the account's Membership page
  3. CZL Connect → Account settings → Integrations → Push credentials → Add credential: enter the Key ID and Team ID and paste the full .p8 contents

Before saving, the credential is tested against the push platform; a rejected credential can't be saved. The key is stored encrypted and never shown again. One .p8 can push to every app in the same developer team, in both sandbox and production. If the credential is later revoked, sends fail with reason credential_rejected, and both the credential card and the app's Push section show a warning.

Enable push for an app

Open the app → Push (visible to the app owner only):

SettingDescription
Enable pushWhen off, device registration and pushes are rejected; registered devices are kept
Push credentialPick one of your account's push credentials
App identifiersiOS Bundle IDs, one per line. Unique site-wide, and verified on save to belong to the selected credential's team; removing an identifier also deletes its device subscriptions

Saving requires passkey confirmation and is recorded in the activity log.

Device endpoints

Base URL: https://connect.czl.net/api/push (use the "Endpoint" shown in the app's Push section). The secret returned at registration is shown only once; afterwards call with Authorization: Bearer <secret>.

MethodPathDescription
POST/v1/subscriptionsRegister a device → 201 {id, secret, url}
PUT/v1/subscriptions/{id}Update the device token / placeholder text and refresh the last-seen time → 204
GET/v1/subscriptions/{id}/pendingFetch ciphertext stashed while pending → {messages: [base64url…]}
POST/v1/subscriptions/{id}/activateActivate and clear the stash → 204
DELETE/v1/subscriptions/{id}Delete → 204

Register / update request body:

{
  "topic": "com.example.app",
  "token": "device token (hex)",
  "sandbox": false,
  "alert": { "title": "Example", "body": "New message" },
  "keys": { "p256dh": "device public key (base64url)", "auth": "auth secret (base64url)" }
}
  • topic must be an app identifier registered in an app with push enabled, otherwise 1008 unknown app; it can't be changed on update
  • sandbox: true for development builds (installed from Xcode), false for TestFlight and App Store builds
  • alert: placeholder text shown when the notification extension can't decrypt the content; the body defaults to New notification
  • keys: optional, only needed when your backend will push per user; the backend encrypts with it
  • Subscriptions not refreshed for 60 days are removed automatically; we recommend a PUT on every app launch. A 404 on PUT means the subscription no longer exists; just register again

url is this device's Web Push URL; give it to the server as the push endpoint.

Pending and verification

A newly registered subscription is "pending": messages pushed meanwhile are only stashed (up to 5 per subscription, for 30 minutes) and no notification is shown. This is for server-side subscription verification, e.g. JMAP sends an encrypted PushVerification right after a PushSubscription is created:

  1. The app, in the foreground, registers the device and gives url to the server to create a push subscription
  2. It polls GET /pending, decrypts the verification code locally and sends it back to the server
  3. It calls POST /activate; only later pushes become system notifications

If no verification is needed, call activate right after registering.

Server-side push subscriptions usually expire; for example, Stalwart's JMAP PushSubscription expires after 7 days by default. The app should renew it with the server when it returns to the foreground and periodically (e.g. every 12 hours) by updating expires, and PUT this service's subscription at the same time to refresh its last-seen time. Renewing doesn't change the push URL, so there's no need to register or verify again; only rebuild when the device keys change (app reinstalled) or this service returns 404.

Web Push delivery

POST {url} (i.e. /api/push/p/{id}) with an RFC 8291 ciphertext body of at most 8 KB. Urgency: low / very-low use power-saving priority, so the system may batch them.

StatusMeaning
201Accepted (stashed while pending)
404Subscription not found; the server should stop using this URL
410The device token is no longer valid (app uninstalled, etc.) and the subscription was deleted; the server should stop using this URL
413Message too large
429Monthly quota used up (1009) or too many requests (1006); retry later
403Push is disabled for the app (1010); the URL works again once re-enabled
502Delivery failed (1007), e.g. no usable credential or the push platform is temporarily unavailable

Push to users (App API)

Your backend calls these with an App API key that has the push:send scope:

1. Link a device to a user: the app passes its subscription id and secret to your backend through your own signed-in channel, and the backend calls

POST /api/v1/app/push/subscriptions/bind
{ "subscriptionId": "...", "secret": "...", "userId": "123" }

userId is the user's CZL Connect user ID, and the user must have authorized your app. When the user signs out in the app, call DELETE /api/v1/app/push/subscriptions/{id}/bind; the subscription stays as an anonymous one.

2. List a user's devices: GET /api/v1/app/push/users/{userId}/devices → {devices: [{id, platform, sandbox, keys: {p256dh, auth}, createdAt, lastPushAt}]}, active devices only.

3. Encrypt per device and send in a batch:

POST /api/v1/app/push/send
{ "messages": [ { "subscriptionId": "...", "payload": "ciphertext (base64url)", "urgency": "high" } ] }

Up to 100 messages per request, each returning {subscriptionId, status, reason}:

statusMeaning
sentHanded to the push platform
goneThe device token is no longer valid and the subscription was deleted; stop sending to it
failedSend failed; reason is credential_missing / credential_rejected / provider_error
skippedThe user unsubscribed from this app's notifications, is banned, or is disabled in this app
inactiveThe subscription isn't activated yet
not_foundThe subscription doesn't exist or doesn't belong to this app
quota_exceededMonthly quota used up
invalidpayload isn't valid base64url or exceeds 8 KB

Unsubscribing applies per app ("this app's notifications") and is shared with broadcast email; anonymous subscriptions (not linked to a user) are unaffected.

App requirements

  • After getting notification permission, obtain the device token and set sandbox for development vs. release builds
  • Generate a P-256 key pair and a 16-byte auth secret; keep the private key in a keychain shared by the main app and the notification extension (App Group), and give the public key and auth to the server (Web Push subscription) or submit them at registration (keys)
  • Add a Notification Service Extension: notifications carry mutable-content: 1; the extension reads the custom key c (base64url ciphertext), decrypts it per RFC 8291 and rewrites the title and body. If it can't decrypt, or the ciphertext was dropped for exceeding about 4 KB, the system shows the registered placeholder text

Rate limits

EndpointKeyed byLimit
Device registrationIP20 / hour
Other device endpointsIP120 / minute
Web Push deliverySubscription120 / minute
App APIAppShared with other App API endpoints

Exceeding a limit returns 429 with code 1006.

Error codes

Device endpoints and Web Push delivery return {"error": "<4-digit code> <English message>"}:

CodeMeaning
1001Invalid parameters (e.g. malformed device token)
1002Missing credentials (no Authorization)
1003Subscription not found / expired
1004Message too large
1005Storage error
1006Too many requests
1007Delivery failed
1008Unknown app
1009Monthly push quota used up
1010Push is disabled for this app

Data and privacy

  • A subscription stores the device token, the SHA-256 of its secret and, optionally, the device public key; notification content is never stored, and ciphertext stashed while pending is discarded after 30 minutes
  • Only daily send counts are recorded, not individual sends
  • Subscriptions are hard-deleted when the token becomes invalid, the subscription or app is deleted, or it hasn't been refreshed for 60 days