Cover: a successful send does not mean receipt

In an illustrative scenario, an operations colleague asks: “We sent 10,000 campaign notifications yesterday. Why did only 200 people open them?” The backend developer checks the logs: “Every API call returned success.”

They mean different things by success. The backend means the vendor accepted the request. Operations means a notification appeared on the user's phone. Offline devices, revoked notification permission, vendor rate limits, battery-saving behavior and expired tokens can intervene.

Based on AppKernia's push design in ADR-0021, this article covers three questions: how to separate push states, how to classify failures, and when to request notification permission.

1. Why connect directly to nine channels?

AppKernia targets the App Store, Google Play, domestic Android stores and HarmonyOS NEXT. The server integrates these channels directly:

  • APNs over HTTP/2 and FCM over HTTP v1;
  • Huawei on Android, Honor, Xiaomi, OPPO, vivo and Meizu;
  • HarmonyOS Push Kit.

There is no third-party push aggregator. Each vendor needs its own adapter, but credentials, limits and error codes remain under the project's control, with fewer layers to investigate when something fails.

Android has two mutually exclusive build variants: android_google and android_china. Bundling every vendor SDK into one package makes size and privacy compliance harder. The overseas build includes FCM; the China build includes domestic vendor channels. Unsupported devices retain in-app messages instead of attempting unsupported push delivery.

2. “Push success” comprises four stages

The four stages of a push message

A state diagram, not a measurement of time spent in each stage.

StateMeaningWho can verify it?
Business event createdA work order was assigned to an engineer, for exampleYour server
Vendor acceptedThe vendor received the send requestThe vendor API response
Device displayedThe notification appeared on the phoneMost channels cannot reliably report this
User openedThe user tapped the notification and opened the appAn app report

In AppKernia, sent means only vendor acceptance. The initial metrics cover acceptance, failure, invalid tokens and app opens. They do not claim device display.

“10,000 accepted, 200 opened” is useful information. Calling those 10,000 messages delivered can send an investigation in the wrong direction.

3. Seven send outcomes determine retry behavior

Seven send outcomes and their handling

Diagram of AppKernia's send-result classifications.

Vendor error codes differ. AppKernia maps them to seven outcomes with explicit handling:

OutcomeMeaningHandling
acceptedThe vendor accepted the requestRecord acceptance
invalid_tokenThe device token is invalidInvalidate the binding immediately and stop sending
throttledThe vendor applied a rate limitBack off according to Retry-After
transientA definite temporary failure, such as a 5xx responseRetry with exponential backoff and jitter
permanentA permanent failureRecord the reason without retrying
auth_config_errorAuthentication or configuration is wrongMark the channel faulty and wait for operator action
unknown_after_writeThe request was written, but its result is unknownDo not replay automatically

The last outcome deserves special attention. A connection can fail after the request has left the client but before its response arrives. The vendor may already have accepted and sent the message, or may not have received it. Retrying can produce duplicate notifications, which are especially confusing for verification codes and order updates.

The rule is: connection failures before writing may be retried; unknown results after writing are not replayed. This favors avoiding duplicates. In-app messages do not depend on successful push delivery, so users can still see them when they open the app.

4. Ask for permission after consent

An app that requests notification permission immediately gives users little reason to agree. Initializing a push SDK and uploading a device token before privacy consent also violates the project's consent boundary.

The ak-push module is the only push entry point available to mobile business code:

Until both required legal consent and the user's master push switch are enabled, do not request notification permission, initialize SDKs, obtain tokens or upload them.

Four gates determine whether a notification can be sent:

The four push-enablement gates

All four conditions must hold.

  1. Global kill switch: stop sends centrally when needed.
  2. App and environment configuration: isolate vendor credentials by tenant, app, environment and vendor; encrypt them and support version rotation.
  3. User subscription: do not send when the user has disabled push.
  4. System permission: deactivate the device binding when notification permission is revoked in system settings.

Logout and account changes also deactivate bindings. Otherwise, user B could receive user A's work-order notifications after signing into the same phone.

Campaign push (news_operations) and service or security push (service_security) are separate protocol categories. Campaign push is off by default and requires notify.operations.publish. HarmonyOS and vivo vendor scenario values are configured separately for these categories.

5. Notification destinations need an allowlist

Tap payloads may contain only a schema version, delivery or message identifier, controlled route_key and opaque resource identifier. They may not contain arbitrary URLs, component names, scripts or instructions for silent background execution.

A payload passing through a vendor server does not make it trustworthy. After a tap, the app uses the resource identifier to fetch the resource with fresh server authorization. If a work order has been reassigned, the user should see an access-denied state rather than the content from the notification.

6. Release acceptance matrix

Compilation, mocks and valid contracts do not replace vendor acceptance. Before enabling a channel, verify:

  • Vendor account entitlements—for example, HarmonyOS Push Kit activation followed by renewed debug and release profiles;
  • Matching credentials, package or Bundle ID and signing fingerprints;
  • Privacy compliance and network review;
  • Real devices in foreground, background, killed-process and offline-recovery states;
  • Permission denial, invalid tokens, taps while signed out and resources the user can no longer access.

Channels that have not passed remain disabled. Tokens are encrypted on the server and deduplicated with an HMAC hash. Logs, audits, metrics and management APIs do not expose raw tokens or credentials.

Closing thoughts

The difficult part of push is explaining each failure clearly. Separate states, classify outcomes and fix retry rules so operations can trust the metrics and users avoid duplicate messages.

ak-push and its ak-permissions dependency are available as free source packages in AppKernia's plugin directory. The full server implementation is in the MIT-licensed repository:

  • Source: github.com/Payhon/AppKernia
  • Documentation: docs.appkernia.com, including push-channel configuration

The source does not include your vendor accounts or signing materials. Each channel still needs acceptance in your project with your own credentials.