Guide · Chapter 4 of 5

RTDN and App Store Server Notifications

Store webhooks keep entitlement state current between purchases — and idempotency and ordering guards keep duplicate or out-of-order delivery from corrupting it.

Why polling doesn't scale

The naive way to keep subscription state fresh is a cron job: walk every active subscriber and re-query the store API. It works on day one and degrades from there. The work grows with your subscriber count, not with the number of things that actually changed — most cycles re-fetch state that is byte-identical to what you already stored. Store APIs are quota-bounded, so a growing base eventually forces you to stretch the interval, which is exactly the wrong direction: the interval is your staleness window. A cancellation, a refund, or a failed renewal is invisible until the next sweep reaches that user.

Both stores solve this by pushing. Google Play emits Real-Time Developer Notifications (RTDN) to a Cloud Pub/Sub topic; Apple posts App Store Server Notifications v2 to an HTTPS endpoint you register. Your backend does work proportional to the number of real lifecycle events, and entitlement state converges within seconds rather than within a polling interval.

Push moves the hard part rather than removing it. A queue that guarantees at-least-once delivery will hand you the same event twice, and a sender that retries on failure will hand you an older event after a newer one. That is what the rest of this chapter is about. For the broader argument for verifying purchases on your server at all, see the server-side purchase verification guide; for the setup mechanics of the Google Play path, see the RTDN docs.

What idempotency and ordering guards protect against

Duplicate delivery

Pub/Sub delivery is at-least-once, and Apple redelivers a notification when your endpoint does not return success. So the same signed payload can arrive two, three, or ten times. Re-applying DID_RENEW to an entitlement is harmless on its own — the row ends up in the same state — but the side effects are not idempotent by nature: each pass would append another renewal entry to the billing timeline, and that timeline feeds churn and revenue analytics.

Tierux records the delivery's own identity on the entitlement row and treats a repeat as a no-op. On the Google Play path that identity is the Pub/Sub messageId; on the Apple path it is the notification's notificationUUID — the two guards work the same way, but the notification types and coverage behind them are not identical between stores, so treat this as "both paths dedupe," not "both paths are at parity" (see the platform support matrix for what actually differs). A notification whose identity matches the one already stored short-circuits with a duplicate outcome before any write, and — because the same key is also armed inside the entitlement store's update transaction — a delivery that slips past the pre-check still loses the write and skips the timeline append. Each lifecycle event is recorded exactly once.

Out-of-order delivery

Ordering is the more dangerous failure. Neither store guarantees that a retry of an older event arrives before a newer one, so a redelivered SUBSCRIPTION_RENEWED can land after the SUBSCRIPTION_EXPIRED that superseded it. Applied naively, last-write-wins hands a lapsed subscriber a live entitlement.

Every notification carries a timestamp — eventTimeMillis on the Google Play path, signedDate on the Apple one — and Tierux stores the newest one it has applied for that entitlement. An event whose timestamp is less than or equal to the stored one is dropped as stale. As with the dedup key, the comparison is armed inside the store's update transaction, not merely checked beforehand, so two deliveries processed concurrently cannot race each other into a last-write-wins outcome. When no notification timestamp has been recorded yet, the guard falls back to the entitlement's last verification time. On the Google Play path that fallback only engages for plausible wall-clock values, so a test fixture can't poison the ordering baseline; the Apple path's fallback does not yet apply that same lower-bound check.

Retry semantics

Guards are only half of it — the response code decides whether the sender tries again. Tierux distinguishes a transient failure from a definitive one. A database blip mid-process returns 503 on either path. On the Google Play path a Play Developer API outage or expired credentials during the state-restoring re-query also returns 503; the Apple path does no store-API re-query at all (expiry comes from the signed payload), so that particular failure mode is Google-Play-only. Either way, Pub/Sub and Apple both redeliver a 503 with backoff, and reprocessing is safe precisely because the guards above make it a no-op if it already landed. Everything else — an unrecognized package, a purchase this backend never recorded, an informational notification type — acknowledges with a success code so the sender stops retrying an event that will never apply. A bad signature is rejected outright rather than retried: it is a forged or corrupt payload, not a transient fault.

# Outcomes that end in "we deliberately did nothing"
duplicate / duplicate_message   same messageId / notificationUUID already applied
stale_event                     event timestamp <= the newest one already applied
product_mismatch                the store's product for this purchase differs from
                                the recorded one — fail closed, never apply
superseded_purchase             (Google Play) an event about a purchase token the
                                entitlement no longer points at, after a plan change

# Outcomes that change state or ask for redelivery
updated                         entitlement patched, timeline event appended once
transient_error / 503           retryable — redelivered with backoff, guards make
                                the reprocess safe

The two notification paths, side by side

Tierux processes lifecycle events on both stores: Google Play RTDN is the primary, production-hardened path, and Apple App Store Server Notifications are implemented with limitations. The support matrix at platform support carries the per-feature status for both.

Google Play — Real-Time Developer Notifications

Google Play publishes to Cloud Pub/Sub, which pushes into Tierux's backend. Tierux operates one shared topic for every customer app, so you point Play Console at it and create no GCP resources of your own; incoming notification JWTs are validated against Tierux's audience claim and the expected Google Play dispatcher service account. Setup detail lives in the RTDN docs.

An RTDN carries only a package name and a purchase token — no user id — so processing resolves the chain package to app, token hash to recorded purchase, product mapping to entitlement, and no-ops on any missing link, because the webhook is global and will receive events for purchases this backend never recorded. Notification types map to entitlement status (cancellation keeps access until expiry; grace period keeps access; account hold and pause revoke it), and for the state-restoring types — purchased, renewed, recovered, restarted — Tierux re-queries the Play Developer API for an authoritative expiry rather than trusting the notification's own view. If Play returns a different product for that token than the one recorded, the event is rejected rather than applied. When Play reports that a subscriber changed plans, the resulting notification is handled explicitly: the new token is resolved back to the superseded one, and later events on the old token can no longer mutate the re-bound entitlement. (This is notification handling for a plan change the store already recorded — Tierux does not yet expose an API to initiate one; see the platform support matrix.)

Apple App Store — Server Notifications v2

Apple's path is not a footnote to this one, and it is not equivalent to it either: App Store Server Notifications are implemented with limitations in the platform support matrix — the same transactional ordering and idempotency guards described above, but less production-hardened than the RTDN path.

Apple posts signed JWS notifications to a per-app endpoint, /api/v1/apple/assn/:appId. Tierux verifies Apple's signature via Apple's own App Store server library, then resolves the purchase by originalTransactionId — the stable identifier across the subscription's life, since each renewal carries a fresh transaction id. The notification type and subtype drive the entitlement effect: a renewal extends access; auto-renew turned off marks the entitlement cancelled while access runs to expiry; a failed renewal keeps access when Apple signals a billing grace period and suspends it when it does not; expiry, refund, and revocation end access; a reversed refund restores it; a developer-granted renewal extension moves the expiry out. Expiry itself is read from the signed payload, using the grace-period expiry when the subscriber is in grace or on hold.

The limitations, stated plainly and current as of the platform support matrix:

Notification pathGoogle Play RTDNApple App Store Server Notifications
Overall status Yes — implemented (RTDN + Pub/Sub JWT) Yes, with limitations — implemented, less production-hardened than the RTDN path
Transport Cloud Pub/Sub push to a shared Tierux topic; OIDC JWT validated HTTPS POST of a signed JWS to /api/v1/apple/assn/:appId
Purchase identity in the payload Package name + purchase token (no user id — resolved via the recorded purchase) originalTransactionId (stable across renewals)
Duplicate guard Pub/Sub messageId, armed in the entitlement-store transaction notificationUUID, armed in the same transaction
Ordering guard eventTimeMillis vs the newest applied event time signedDate vs the newest applied event time
Expiry source Re-query of the Play Developer API on state-restoring types Read from the signed notification payload; no additional server-API re-query on this path
Transient failure handling 503 — Pub/Sub redelivers; reprocessing is guarded 503 — Apple redelivers with backoff; reprocessing is guarded
Endpoint setup Point Play Console at Tierux's shared Pub/Sub topic Manual App Store Connect URL entry, guided by the setup_app_store MCP tool

Per-feature, per-platform status is maintained in the platform support matrix.

Related reading

Docs: RTDN setup

Pointing Play Console at the shared Pub/Sub topic, JWT validation, and the notification-type mapping.

Docs: Webhooks

Forwarding the same lifecycle events to your own backend as signed HTTP POSTs.

Glossary: RTDN

What Real-Time Developer Notifications are and which events Google Play emits.

Glossary: App Store Server Notifications

Apple's signed server-to-server messages, and how they differ from RTDN.

Server-side purchase verification

Why the client can't be trusted to report a purchase, and how verification works on both stores.

Platform support matrix

The canonical per-feature status for Google Play and Apple App Store.

Developers

Entitlements in one REST call, set up by your AI agent over MCP — the home base for the API this guide is written against.

← Back to the server-side verification guide

Entitlements in one REST call

Free tier — unlimited apps, 1 paywall. No credit card, no revenue share.

Start free