# Validate Google Play purchases

**URL:** https://heroiclabs.com/docs/nakama/concepts/iap-validation/google/
**Summary:** Step-by-step guide to configuring and validating Google Play purchases and subscriptions with Nakama, including handling refunds and subscription changes through real-time notifications.
**Keywords:** iap, in-app purchases, validation, google, Android, play store
**Categories:** nakama, google, iap-validation

---


# Validate Google Play purchases

This guide shows you how to set up and validate Google Play purchases and subscriptions with Nakama.

## Before you begin

Make sure you have:

- A Google Play Console account
- Access to Google Cloud Console for creating service accounts
- An Android app configured for in-app purchases in Google Play Console

## Configure Google Play credentials

To validate receipts against the Play Store, you'll need the `client_email` and `private_key` from a Google Cloud Service Account that has been granted access to your app in the Google Play Console.

{{< pretitle "Step 1" >}}

### Create a Google Cloud Service Account

1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
2. Select your project (or create a new one).
3. Navigate to **IAM & Admin** > **Service Accounts**.
4. Click **Create Service Account**.
5. Enter a name (e.g., "Nakama IAP Validator") and description.
6. Click **Create and Continue**.
7. Click **Done**.

![Create Service Account](images/google_iap_create_service_account.png)

{{< note "important" >}}
The service account doesn't require any GCP IAM roles. The only permissions Nakama needs are the ones you grant in Google Play Console later on.
{{< /note >}}

{{< pretitle "Step 2" >}}

### Create a JSON key

1. Back on the **Service Accounts** page, make a note of your newly created service account's email. You'll need it later.
2. Click on the service account in the list.
3. Navigate to the **Keys** tab.
4. Click **Add Key** > **Create new key**.
5. Select **JSON** as the key type.
6. Click **Create** - a JSON file will be downloaded to your computer.

The JSON file contains the `client_email` and `private_key` that Nakama needs. Keep this file secure.

![Create JSON Key](images/google_iap_create_json_key.png)

{{< pretitle "Step 3" >}}

### Enable the Google Play Developer API

1. Navigate to the [Google Play Developer API page](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com) in Google Cloud Console.
2. Click **Enable**.

{{< pretitle "Step 4" >}}

### Grant Service Account access in Google Play Console

1. Open the [Google Play Console](https://play.google.com/console/).
2. Navigate to **Users and permissions**.
3. Click **Invite new users**.
4. Enter the email address of the service account you created earlier.
5. Under **App permissions** (or **Account permissions**), grant these permissions:.
   - **View app information and download bulk reports (read only)**
   - **View financial data, orders, and cancellation survey responses**
   - **Manage orders and subscriptions**

6. Click **Invite user** to save the permissions

{{< note "warning" >}}
It may take up to 24 hours for the service account permissions to fully propagate through Google's systems
{{< /note >}}

{{< pretitle "Step 5" >}}

### Configure Nakama

From the JSON key file you downloaded, extract the `client_email` and `private_key` values and configure them in Nakama:

- Set [iap.google.client_email](../../../getting-started/configuration/#google.client_email) to the email address from the JSON file.
- Set [iap.google.private_key](../../../getting-started/configuration/#google.private_key) to the private key from the JSON file (keep the `
-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` markers).

```yaml
iap:
  google:
    client_email: "nakama-iap-validator@your-project.iam.gserviceaccount.com"
    private_key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBg...your-key-here...\n-----END PRIVATE KEY-----\n"
```

{{< note "important" "Key format for Heroic Cloud">}}
When setting `iap.google.private_key` in the Heroic Cloud deployment config UI, paste the private key with real line breaks, not the `\n`-escaped format shown in the YAML example above. Copy the value of the `private_key` field directly from your downloaded file, including the `-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----` markers.
{{< /note >}}

### Troubleshooting

If validation fails with permission errors:

- Verify the service account has been granted the correct permissions in Google Play Console.
- Wait 24 hours after granting permissions for changes to propagate.
- Ensure the `client_email` and `private_key` exactly match the values in your JSON key file.
- Check that you're using the correct Google Play Console account that owns the app.

## Validate a purchase

To validate a Google Play purchase, you'll send the purchase data returned by the Google Play Billing library to Nakama's validation endpoint. Nakama verifies the purchase with Google's servers and returns the validated purchase details.

{{< note "important" >}}
The Google receipt expected is the string returned by `Purchase.getOriginalJson()`. See the official [Google Developer docs](<https://developer.android.com/reference/com/android/billingclient/api/Purchase#getOriginalJson()>) for more details.
{{< / note >}}

{{< code type="client" >}}

```bash
curl "http://127.0.0.1:7350/v2/iap/purchase/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
```

{{< / code >}}

{{< code type="client" framework="defold" >}}

```lua
local function validate_receipt(receipt)
  local result = client.validate_purchase_google(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
```

{{< / code >}}

{{< code type="client" >}}

```csharp
string googleReceipt = "<receipt>";
var response = await client.ValidatePurchaseGoogleAsync(session, googleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
```

{{< / code >}}

{{< code type="client" >}}

```swift
let googleReceipt = "<receipt>"

let response = try await client.validatePurchaseGoogle(session: session, receipt: googleReceipt)

for purchase in response.validatedPurchases {
    print("Validated purchase: \(purchase)")
}
```

{{< / code >}}

{{< missing type="client" lang="dart" />}}

{{< code type="client" >}}

```javascript
const googleReceipt = "<receipt>";
const result = await client.validatePurchaseGoogle(session, googleReceipt);
result.validatedPurchases.forEach((validatedPurchase) => {
  console.info("Validated purchase:", validatedPurchase);
});
```

{{< / code >}}

{{< code type="client" framework="godot3" >}}

```gdscript
var google_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_google_async(session, google_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
```

{{< / code >}}

{{< code type="client" framework="godot4" >}}

```gdscript
var google_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = await client.validate_purchase_google_async(session, google_receipt)
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
```

{{< / code >}}

{{< code type="client" >}}

```shell
POST /v2/iap/purchase/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}
```

{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="java" />}}

Refer to the function reference page for the provided runtime [purchase validation functions](../../../server-framework/typescript-runtime/function-reference/#purchaseValidateGoogle).

## Validate a subscription

Subscription validation for Google Play works similarly to purchase validation, returning subscription-specific information like expiry time, active status, and auto-renew preferences.

{{< code type="client" >}}

```csharp
string googleReceipt = "<receipt>";
var response = await client.ValidateSubscriptionGoogleAsync(session, googleReceipt);

System.Console.WriteLine("Validated subscription: " + response.ValidatedSubscription);
```

{{< / code >}}

{{< code type="client" >}}

```swift
let googleReceipt = "<receipt>"

let response = try await client.validateSubscriptionGoogle(session: session, receipt: googleReceipt)

print("Validated subscription: \(response.validatedSubscription)")
```

{{< / code >}}

{{< missing type="client" lang="dart" />}}

{{< code type="client" framework="godot3" >}}

```gdscript
var google_receipt = "..."
var response : NakamaAPI.ApiValidateSubscriptionResponse = yield(client.validate_subscription_google_async(session, google_receipt), "completed")

if response.is_exception():
    print("An error occurred: %s" % response)
    return

print("Validated subscription: %s" % response.validated_subscription)
```

{{< / code >}}

{{< code type="client" framework="godot4" >}}

```gdscript
var google_receipt = "..."
var response : NakamaAPI.ApiValidateSubscriptionResponse = await client.validate_subscription_google_async(session, google_receipt)

if response.is_exception():
    print("An error occurred: %s" % response)
    return

print("Validated subscription: %s" % response.validated_subscription)
```

{{< / code >}}

{{< code type="client" >}}

```bash
curl "http://127.0.0.1:7350/v2/iap/subscription/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
```

{{< / code >}}

{{< code type="client" >}}

```shell
POST /v2/iap/subscription/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}
```

{{< / code >}}

{{< code type="client" framework="defold" >}}

```lua

local google_receipt = "<receipt>";
local response = client.validate_purchase_google(receipt)
for i,purchase in ipairs(response.validated_purchase) do
  pprint("Validated purchase: ", purchase)
end
```

{{< / code >}}

{{< code type="client" >}}

```javascript
const googleReceipt = "<receipt>";
const result = await client.validateSubscriptionGoogle(session, googleReceipt);
console.info("Validated subscription:", result.validatedSubscription);
```

{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="java" />}}

Refer to the function reference page for the provided runtime [subscription validation functions](../../../server-framework/typescript-runtime/function-reference/#subscriptionValidateGoogle).

## Handle refunds and subscription changes

Google Play supports [real-time developer notifications](https://developer.android.com/google/play/billing/getting-ready#configure-rtdn) to monitor IAP state changes in real-time. Nakama can receive these notifications to track subscription renewals, expirations, cancellations, and refunds automatically.

### Set up notification callbacks

To activate the callback URL, set the [notifications_endpoint_id](../../../getting-started/configuration/#google.notifications_endpoint_id) configuration, which creates the following endpoint path: `/v2/console/google/subscriptions/<notifications_endpoint_id>`.

Configure this URL in the [Google Play Console](https://developer.android.com/google/play/billing/getting-ready#configure-rtdn) as a Cloud Pub/Sub topic that pushes to this endpoint.

### Automatic state updates

Once you've configured the callback URL, Nakama automatically updates the state of any purchase or subscription that was previously validated through the validation APIs above. This keeps subscription expiry times, active status, and other metadata synchronized with Google Play.

{{< note "important" >}}
Notifications only work for purchases and subscriptions that you've already validated through Nakama's client APIs. If Google sends a notification for a purchase Nakama hasn't seen before, it'll be ignored.
{{< / note >}}

### Notification types

Nakama normalizes Google's notifications into 5 simplified types:

| Type         | Description                                     |
| ------------ | ----------------------------------------------- |
| `SUBSCRIBED` | Initial subscription purchase or resubscription |
| `RENEWED`    | Subscription successfully auto-renewed          |
| `EXPIRED`    | Subscription expired and will not renew         |
| `CANCELLED`  | Subscription cancelled by user or Google Play   |
| `REFUNDED`   | Purchase or subscription refunded               |

These normalized types match those used for Apple, making it easier to handle notifications consistently across platforms.

### Implement notification hooks

You can register custom code to respond to notifications for purchases and subscriptions. The hooks fire **after** Nakama has automatically updated the purchase/subscription state in the database.

Use these hooks to implement your own business logic when IAP states change—like revoking player access when a subscription expires, sending analytics events, or triggering in-game rewards for renewals.

#### Purchase notifications

Register a hook to handle purchase refunds. The `purchase` parameter contains the validated purchase data. See [Response contents](../#response-contents) for available fields.

{{< code type="server" >}}

```go
if err := initializer.RegisterPurchaseNotificationGoogle(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, notificationType runtime.NotificationType, purchase *api.ValidatedPurchase, providerPayload *runtime.PurchaseV2GoogleResponse) error {
    logger.Info("Google purchase notification: %s", notificationType.String())

    // For purchases, notificationType will always be REFUNDED since that's the only
    // purchase notification Google sends.
    if notificationType == runtime.IAPNotificationRefunded {
        // Handle refund - e.g., revoke access, update analytics
        logger.Info("Purchase refunded for user: %s, product: %s", purchase.UserId, purchase.ProductId)

        // providerPayload contains detailed Google Play response data if needed
    }

    return nil
}); err != nil {
    return err
}
```

{{< / code >}}

{{< code type="server" >}}

```typescript
let registerPurchaseNotificationGoogle: nkruntime.RegisterPurchaseNotificationGoogleFunction =
  function (
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    notificationType: nkruntime.NotificationType,
    purchase: nkruntime.ValidatedPurchase,
    providerPayload: nkruntime.PurchaseV2GoogleResponse
  ) {
    logger.info("Google purchase notification: %s", notificationType);

    if (notificationType == nkruntime.NotificationType.REFUNDED) {
      logger.info(
        "Purchase refunded for user: %s, product: %s",
        purchase.userId,
        purchase.productId
      );
      // Handle refund logic
    }
  };

initializer.registerPurchaseNotificationGoogle(
  registerPurchaseNotificationGoogle
);
```

{{< / code >}}

{{< code type="server" >}}

```lua
local function purchase_notification_google(context, logger, nk, notification_type, purchase, provider_payload)
    logger.info(("Google purchase notification: %s"):format(notification_type))

    if notification_type == "REFUNDED" then
        logger.info(("Purchase refunded for user: %s, product: %s"):format(purchase.user_id, purchase.product_id))
        -- Handle refund logic
    end
end

nk.register_purchase_notification_google(purchase_notification_google)
```

{{< / code >}}

#### Subscription notifications

Register a hook to handle all subscription state changes. The `subscription` parameter contains the validated subscription data - see [Response contents](../#response-contents) for available fields.

{{< code type="server" >}}

```go
if err := initializer.RegisterSubscriptionNotificationGoogle(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, notificationType runtime.NotificationType, subscription *api.ValidatedSubscription, providerPayload *runtime.SubscriptionV2GoogleResponse) error {
    logger.Info("Google subscription notification: %s", notificationType.String())

    // Handle different notification types
    switch notificationType {
    case runtime.IAPNotificationSubscribed:
        logger.Info("New subscription: %s", subscription.ProductId)
        // Handle new subscription - e.g., grant access, send welcome message

    case runtime.IAPNotificationRenewed:
        logger.Info("Subscription renewed: %s", subscription.ProductId)
        // Handle renewal - e.g., extend access, update analytics

    case runtime.IAPNotificationExpired:
        logger.Info("Subscription expired: %s", subscription.ProductId)
        // Handle expiration - e.g., revoke access, send re-engagement message

    case runtime.IAPNotificationCancelled:
        logger.Info("Subscription cancelled: %s", subscription.ProductId)
        // Handle cancellation - e.g., schedule access revocation, send feedback survey

    case runtime.IAPNotificationRefunded:
        logger.Info("Subscription refunded: %s", subscription.ProductId)
        // Handle refund - e.g., immediately revoke access, update analytics
    }

    // The subscription object already contains updated state (expiry time, etc.)
    // providerPayload contains detailed Google Play Billing API v2 response for custom processing if needed

    return nil
}); err != nil {
    return err
}
```

{{< / code >}}

{{< code type="server" >}}

```typescript
let registerSubscriptionNotificationGoogle: nkruntime.RegisterSubscriptionNotificationGoogleFunction =
  function (
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    notificationType: nkruntime.NotificationType,
    subscription: nkruntime.ValidatedSubscription,
    providerPayload: nkruntime.SubscriptionV2GoogleResponse
  ) {
    logger.info("Google subscription notification: %s", notificationType);

    switch (notificationType) {
      case nkruntime.NotificationType.SUBSCRIBED:
        logger.info("New subscription: %s", subscription.productId);
        break;
      case nkruntime.NotificationType.RENEWED:
        logger.info("Subscription renewed: %s", subscription.productId);
        break;
      case nkruntime.NotificationType.EXPIRED:
        logger.info("Subscription expired: %s", subscription.productId);
        break;
      case nkruntime.NotificationType.CANCELLED:
        logger.info("Subscription cancelled: %s", subscription.productId);
        break;
      case nkruntime.NotificationType.REFUNDED:
        logger.info("Subscription refunded: %s", subscription.productId);
        break;
    }
  };

initializer.registerSubscriptionNotificationGoogle(
  registerSubscriptionNotificationGoogle
);
```

{{< / code >}}

{{< code type="server" >}}

```lua
local function subscription_notification_google(context, logger, nk, notification_type, subscription, provider_payload)
    logger.info(("Google subscription notification: %s"):format(notification_type))

    if notification_type == "SUBSCRIBED" then
        logger.info(("New subscription: %s"):format(subscription.product_id))
    elseif notification_type == "RENEWED" then
        logger.info(("Subscription renewed: %s"):format(subscription.product_id))
    elseif notification_type == "EXPIRED" then
        logger.info(("Subscription expired: %s"):format(subscription.product_id))
    elseif notification_type == "CANCELLED" then
        logger.info(("Subscription cancelled: %s"):format(subscription.product_id))
    elseif notification_type == "REFUNDED" then
        logger.info(("Subscription refunded: %s"):format(subscription.product_id))
    end
end

nk.register_subscription_notification_google(subscription_notification_google)
```

{{< / code >}}

## See also

- [About in-app purchase validation](../#)
- [Validate Apple App Store purchases](../apple/)
- [Purchases function reference](../../../server-framework/go-runtime/function-reference/#Purchases)
