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:
| Type | Name | What happened | Recommended action |
|---|---|---|---|
| 1 | RECOVERED | Payment recovered after billing retry | Restore access |
| 2 | RENEWED | Subscription renewed | Extend access period |
| 3 | CANCELED | Subscription cancelled (voluntary or involuntary) | Access continues until expiry; flag for win-back |
| 4 | PURCHASED | New subscription | Grant access (server-side backup for client flow) |
| 5 | ON_HOLD | Account hold — payment failed, access paused | Suspend access, notify user to update payment |
| 6 | IN_GRACE_PERIOD | Grace period started — payment failed, access continues | Keep access, notify user to update payment |
| 7 | RESTARTED | User restarted subscription from account hold | Restore access |
| 9 | DEFERRED | Renewal deferred (e.g., pending price change acceptance) | No immediate action |
| 10 | PAUSED | User paused their subscription | Suspend access at period end |
| 12 | REVOKED | Subscription revoked (refund or policy violation) | Revoke access immediately |
| 13 | EXPIRED | Subscription expired | Revoke 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:
- Go to Pub/Sub → Topics
- Click Create topic
- Name it (e.g.,
play-subscription-notifications) - Click Create
2. Grant Google Play publish permission
On the topic you just created:
- Click the topic name → Permissions tab
- Click Add principal
- Enter:
google-play-developer-notifications@system.gserviceaccount.com - Assign the Pub/Sub Publisher role
- 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.
| Push | Pull | |
|---|---|---|
| Setup | Provide an HTTPS endpoint | No endpoint needed |
| Latency | Near-instant delivery | Depends on polling frequency |
| Firewall | Endpoint must be publicly reachable | Works behind firewalls |
| Retries | Pub/Sub retries unacknowledged messages | You control retry logic |
4. Connect to Google Play Console
- Open Google Play Console
- Go to Monetize → Monetization setup
- In the Real-time developer notifications section, enter the full topic name:
projects/YOUR_PROJECT_ID/topics/play-subscription-notifications - Click Send test notification to verify the pipeline
- 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.