starti.app
How-to Guides

Handle Google Server Notifications

Receive and process Google Play Real-Time Developer Notifications (RTDN) to track subscription lifecycle events on your backend

Handle Google Server Notifications

When a user subscribes, renews, cancels, or gets refunded on Google Play, Google publishes a notification to a Cloud Pub/Sub topic you configure. These are called Real-Time Developer Notifications (RTDN).

Unlike Apple (which sends HTTP POST requests directly to your server), Google uses Pub/Sub as an intermediary. Your server either receives push deliveries from Pub/Sub or pulls messages from it.

Without RTDN, your backend only knows about subscription changes when the app explicitly checks. RTDN lets you react immediately to renewals, cancellations, billing issues, and refunds.

Notification types

Each notification contains an integer notificationType. Here are the most common ones and what to do with them:

TypeNameWhat happenedRecommended action
1RECOVEREDPayment recovered after billing retryRestore access
2RENEWEDSubscription renewedExtend access period
3CANCELEDSubscription cancelled (voluntary or involuntary)Access continues until expiry; flag for win-back
4PURCHASEDNew subscriptionGrant access (server-side backup for client flow)
5ON_HOLDAccount hold — payment failed, access pausedSuspend access, notify user to update payment
6IN_GRACE_PERIODGrace period started — payment failed, access continuesKeep access, notify user to update payment
7RESTARTEDUser restarted subscription from account holdRestore access
9DEFERREDRenewal deferred (e.g., pending price change acceptance)No immediate action
10PAUSEDUser paused their subscriptionSuspend access at period end
12REVOKEDSubscription revoked (refund or policy violation)Revoke access immediately
13EXPIREDSubscription expiredRevoke access

This table covers the most common types. See Google's SubscriptionNotification reference for the full list.

Decode and verify

Message structure

Pub/Sub delivers a JSON message. If you're using push delivery, the HTTP POST body looks like this:

{
  "message": {
    "data": "eyJ2ZXJzaW9uIjoiMS4wIiwicGFja2FnZU5hbWUiOiJjb20uZXhhbXBsZSIs...",
    "messageId": "123456789",
    "publishTime": "2026-04-15T10:00:00Z"
  },
  "subscription": "projects/my-project/subscriptions/play-notifications"
}

The message.data field is a base64-encoded JSON string. Decode it to get the DeveloperNotification:

{
  "version": "1.0",
  "packageName": "com.example.myapp",
  "eventTimeMillis": "1744707600000",
  "subscriptionNotification": {
    "version": "1.0",
    "notificationType": 2,
    "purchaseToken": "abc123..."
  }
}

Get full subscription state

The notification only tells you what happened — not the full subscription state. Always call the Google Play Developer API to get the complete picture:

import { google } from "googleapis";

const auth = new google.auth.GoogleAuth({
  keyFile: "./service-account.json",
  scopes: ["https://www.googleapis.com/auth/androidpublisher"],
});

const androidPublisher = google.androidpublisher({ version: "v3", auth });

const subscription = await androidPublisher.purchases.subscriptionsv2.get({
  packageName: "com.example.myapp",
  token: purchaseToken,
});

console.log(subscription.data);
// {
//   lineItems: [{ productId, expiryTime, ... }],
//   subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",
//   ...
// }

Verify push requests

If you use push delivery, verify that requests actually come from Pub/Sub. Configure your Pub/Sub subscription with an OIDC authentication token, then verify the bearer token in incoming requests:

import { OAuth2Client } from "google-auth-library";

const authClient = new OAuth2Client();

async function verifyPubSubToken(req) {
  const bearer = req.headers.authorization?.split(" ")[1];
  if (!bearer) throw new Error("Missing authorization header");

  const ticket = await authClient.verifyIdToken({
    idToken: bearer,
    audience: process.env.PUBSUB_AUDIENCE, // your endpoint URL
  });

  const claim = ticket.getPayload();
  // Optionally verify claim.email matches your Pub/Sub service account
  return claim;
}

Without push authentication, anyone who discovers your endpoint URL can send fake notifications. Always verify the OIDC token in production.

Set up Pub/Sub and Google Play Console

1. Create a Pub/Sub topic

In the Google Cloud Console:

  1. Go to Pub/Sub → Topics
  2. Click Create topic
  3. Name it (e.g., play-subscription-notifications)
  4. Click Create

2. Grant Google Play publish permission

On the topic you just created:

  1. Click the topic name → Permissions tab
  2. Click Add principal
  3. Enter: google-play-developer-notifications@system.gserviceaccount.com
  4. Assign the Pub/Sub Publisher role
  5. Click Save

3. Create a subscription

Still in Google Cloud Console, create a subscription on the topic:

  • Push (recommended for most setups): Enter your endpoint URL (e.g., https://api.example.com/webhooks/google). Enable authentication with an OIDC token.
  • Pull: No endpoint needed — your server polls for messages. Better if your server is behind a firewall.
PushPull
SetupProvide an HTTPS endpointNo endpoint needed
LatencyNear-instant deliveryDepends on polling frequency
FirewallEndpoint must be publicly reachableWorks behind firewalls
RetriesPub/Sub retries unacknowledged messagesYou control retry logic

4. Connect to Google Play Console

  1. Open Google Play Console
  2. Go to Monetize → Monetization setup
  3. In the Real-time developer notifications section, enter the full topic name: projects/YOUR_PROJECT_ID/topics/play-subscription-notifications
  4. Click Send test notification to verify the pipeline
  5. Click Save

Node.js reference implementation

A complete Express handler for Pub/Sub push delivery:

import express from "express";
import { google } from "googleapis";
import { OAuth2Client } from "google-auth-library";

const app = express();
app.use(express.json());

// Google Play Developer API client
const auth = new google.auth.GoogleAuth({
  keyFile: "./service-account.json",
  scopes: ["https://www.googleapis.com/auth/androidpublisher"],
});
const androidPublisher = google.androidpublisher({ version: "v3", auth });

// OIDC token verifier for Pub/Sub push auth
const authClient = new OAuth2Client();

app.post("/webhooks/google", async (req, res) => {
  // Verify the Pub/Sub push token
  try {
    const bearer = req.headers.authorization?.split(" ")[1];
    await authClient.verifyIdToken({
      idToken: bearer,
      audience: process.env.PUBSUB_AUDIENCE, // your endpoint URL
    });
  } catch {
    // Return 200 to prevent Pub/Sub from retrying invalid tokens endlessly
    return res.sendStatus(200);
  }

  // Acknowledge immediately — Pub/Sub retries unacknowledged messages
  res.sendStatus(200);

  try {
    // Decode the Pub/Sub message
    const data = JSON.parse(
      Buffer.from(req.body.message.data, "base64").toString()
    );

    const notification = data.subscriptionNotification;
    if (!notification) {
      // Not a subscription notification (could be a test or voided purchase)
      return;
    }

    const { notificationType, purchaseToken } = notification;

    // Get the full subscription state from the API
    const { data: subscription } =
      await androidPublisher.purchases.subscriptionsv2.get({
        packageName: data.packageName,
        token: purchaseToken,
      });

    switch (notificationType) {
      case 4: // PURCHASED
        await grantAccess(subscription);
        break;

      case 1: // RECOVERED
      case 2: // RENEWED
      case 7: // RESTARTED
        await extendAccess(subscription);
        break;

      case 3: // CANCELED
        await flagForWinBack(subscription);
        break;

      case 5: // ON_HOLD
      case 10: // PAUSED
        await suspendAccess(subscription);
        break;

      case 6: // IN_GRACE_PERIOD
        await notifyPaymentIssue(subscription);
        break;

      case 12: // REVOKED
      case 13: // EXPIRED
        await revokeAccess(subscription);
        break;

      default:
        console.log(`Unhandled notification type: ${notificationType}`);
    }
  } catch (error) {
    console.error("Failed to process Google notification:", error);
  }
});

Acknowledge immediately. Return 200 before processing. Pub/Sub retries unacknowledged messages, which leads to duplicate processing if your handler is slow.

Idempotency. Pub/Sub guarantees at-least-once delivery — you may receive the same message more than once. Deduplicate using message.messageId from the Pub/Sub envelope, or a combination of purchaseToken + notificationType.

Always call the API. The notification only tells you the event type. The purchases.subscriptionsv2.get response contains the actual subscription state, expiry time, and line items. Make access decisions based on the API response, not the notification type alone.

See also

On this page