Skip to content
cx-lukas-salkauskas-xPublic

About

Python In-app purchase validator for Apple AppStore and GooglePlay.

Topics

Resources

Code of conduct

Security policy

Stars

218 stars

Watchers

11 watching

Forks

Repository files navigation

InAppPy

ci pypi downloads

Table of contents

  1. Introduction
  2. Installation
  3. Google Play (receipt + signature)
  4. Google Play (verification)
    • Setting up Google Service Account Credentials
    • Usage Example (with file path)
    • Usage Example (with credentials dictionary)
  5. Google Play (verification with result)
  6. Google Play (consuming products)
  7. App Store (receipt + using optional shared-secret)
  8. App Store Response (validation_result / raw_response) example
  9. App Store, asyncio version (available in the inapppy.asyncio package)
  10. Development
  11. Donate

1. Introduction

In-app purchase validation library for Apple AppStore and GooglePlay (App Store validator have async support!). Works on python3.6+

2. Installation

pip install inapppy

3. Google Play (validates receipt against provided signature using RSA)

from inapppy import GooglePlayValidator, InAppPyValidationError


bundle_id = 'com.yourcompany.yourapp'
api_key = 'API key from the developer console'
validator = GooglePlayValidator(bundle_id, api_key)

try:
    # receipt means `androidData` in result of purchase
    # signature means `signatureAndroid` in result of purchase
    validation_result = validator.validate('receipt', 'signature')
except InAppPyValidationError:
    # handle validation error
    pass

An additional example showing how to authenticate using dict credentials instead of loading from a file

import json
from inapppy import GooglePlayValidator, InAppPyValidationError


bundle_id = 'com.yourcompany.yourapp'
# Avoid hard-coding credential data in your code. This is just an example.
api_credentials = json.loads('{'
                             '   "type": "service_account",'
                             '   "project_id": "xxxxxxx",'
                             '   "private_key_id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",'
                             '   "private_key": "-----BEGIN PRIVATE KEY-----\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==\n-----END PRIVATE KEY-----\n",'
                             '   "client_email": "XXXXXXXXX@XXXXXXXX.XXX",'
                             '   "client_id": "XXXXXXXXXXXXXXXXXX",'
                             '   "auth_uri": "https://accounts.google.com/o/oauth2/auth",'
                             '   "token_uri": "https://oauth2.googleapis.com/token",'
                             '   "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",'
                             '   "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXXXXXXXXXX.iam.gserviceaccount.com"'
                             ' }')
validator = GooglePlayValidator(bundle_id, api_credentials)

try:
    # receipt means `androidData` in result of purchase
    # signature means `signatureAndroid` in result of purchase
    validation_result = validator.validate('receipt', 'signature')
except InAppPyValidationError:
    # handle validation error
    pass

4. Google Play verification

Setting up Google Service Account Credentials

Before using Google Play verification, you need to set up a Google Service Account and obtain the credentials file. This section explains what GOOGLE_SERVICE_ACCOUNT_KEY_FILE is and how to obtain it.

What is GOOGLE_SERVICE_ACCOUNT_KEY_FILE?

GOOGLE_SERVICE_ACCOUNT_KEY_FILE is a JSON file containing a service account's private key and credentials. This file authorizes your application to access the Google Play Developer API to verify in-app purchases and subscriptions.

The credentials can be provided in two ways:

  1. As a file path (string): Path to the JSON key file downloaded from Google Cloud Console
  2. As a dictionary (dict): The parsed JSON content of the key file

How to obtain the Service Account Key File:

  1. Link Google Cloud Project to Google Play Console
    • Go to Google Play Console
    • Select your app
    • Navigate to Settings → Developer account → API access
    • If you haven't linked a project yet, click Link to create or link a Google Cloud project
    • Accept the terms and conditions
  2. Create a Service Account
    • In the API access page, scroll to Service accounts
    • Click Create new service account or Learn how to create service accounts (this will take you to Google Cloud Console)
    • In Google Cloud Console:
      • Go to IAM & Admin → Service Accounts
      • Click + CREATE SERVICE ACCOUNT
      • Enter a name (e.g., "InAppPy Validator") and description
      • Click CREATE AND CONTINUE
      • Skip granting roles (not needed for this step)
      • Click DONE
  3. Grant Permissions in Google Play Console
    • Return to Google Play Console → Settings → Developer account → API access
    • Find your newly created service account in the list
    • Click Grant access
    • Under App permissions, select your app
    • Under Account permissions, enable:
      • View financial data (for viewing purchase/subscription info)
      • Manage orders and subscriptions (if you need to consume products or manage subscriptions)
    • Click Invite user and then Send invitation
  4. Download the JSON Key File
    • Go back to Google Cloud Console → IAM & Admin → Service Accounts
    • Click on your service account email
    • Go to the KEYS tab
    • Click ADD KEY → Create new key
    • Select JSON as the key type
    • Click CREATE
    • The JSON key file will be automatically downloaded
    • IMPORTANT: Store this file securely! It contains a private key and cannot be recovered if lost
  5. Important Notes
    • The JSON key file should contain fields like: type, project_id, private_key_id, private_key, client_email, etc.
    • Keep this file secure and never commit it to version control
    • In some cases, you may need to create at least one product in your Google Play Console before the API access works properly
    • It may take a few minutes for permissions to propagate after granting access

Example JSON key file structure:

{
  "type": "service_account",
  "project_id": "your-project-id",
  "private_key_id": "a1b2c3d4e5f6...",
  "private_key": "-----BEGIN PRIVATE KEY-----\nYourPrivateKeyHere\n-----END PRIVATE KEY-----\n",
  "client_email": "your-service-account@your-project.iam.gserviceaccount.com",
  "client_id": "123456789",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..."
}

Usage Example (with file path)

from inapppy import GooglePlayVerifier, errors


def google_validator(receipt):
    """
    Accepts receipt, validates in Google.
    """
    purchase_token = receipt['purchaseToken']
    product_sku = receipt['productId']

    # Pass the path to your service account JSON key file
    verifier = GooglePlayVerifier(
        GOOGLE_BUNDLE_ID,
        '/path/to/your-service-account-key.json',  # Path to the JSON key file
    )
    response = {'valid': False, 'transactions': []}
    try:
        result = verifier.verify(
            purchase_token,
            product_sku,
                            is_subscription=True
        )
        response['valid'] = True
        response['transactions'].append(
            (result['orderId'], product_sku)
        )
    except errors.GoogleError as exc:
        logging.error('Purchase validation failed {}'.format(exc))
    return response

Usage Example (with credentials dictionary)

import json
from inapppy import GooglePlayVerifier, errors


def google_validator(receipt):
    """
    Accepts receipt, validates in Google using dict credentials.
    """
    purchase_token = receipt['purchaseToken']
    product_sku = receipt['productId']

    # Load credentials from environment variable or secure storage
    # NEVER hard-code credentials in your source code!
    credentials_json = os.environ.get('GOOGLE_SERVICE_ACCOUNT_JSON')
    credentials_dict = json.loads(credentials_json)

    # Pass the credentials as a dictionary
    verifier = GooglePlayVerifier(
        GOOGLE_BUNDLE_ID,
        credentials_dict,  # Dictionary containing the JSON key data
    )
    response = {'valid': False, 'transactions': []}
    try:
        result = verifier.verify(
            purchase_token,
            product_sku,
                            is_subscription=True
        )
        response['valid'] = True
        response['transactions'].append(
            (result['orderId'], product_sku)
        )
    except errors.GoogleError as exc:
        logging.error('Purchase validation failed {}'.format(exc))
    return response

5. Google Play verification (with result)

Alternative to .verify method, instead of raising an error result class will be returned.

Note: See section 4 for instructions on setting up GOOGLE_SERVICE_ACCOUNT_KEY_FILE.

from inapppy import GooglePlayVerifier, errors


def google_validator(receipt):
    """
    Accepts receipt, validates in Google.
    """
    purchase_token = receipt['purchaseToken']
    product_sku = receipt['productId']

    # Use the service account credentials (see section 4 for setup)
    verifier = GooglePlayVerifier(
        GOOGLE_BUNDLE_ID,
        GOOGLE_SERVICE_ACCOUNT_KEY_FILE,  # Path to JSON key file or dict
    )
    response = {'valid': False, 'transactions': []}

    result = verifier.verify_with_result(
        purchase_token,
        product_sku,
        is_subscription=True
    )

    # result contains data
    raw_response = result.raw_response
    is_canceled = result.is_canceled
    is_expired = result.is_expired

    return result

6. Google Play (consuming products)

After validating a purchase, you can consume a one-time product to prevent refunds and allow the user to purchase it again. This is a separate operation that should be called after verification to handle cases like power outages between validation and consumption.

Note: See section 4 for instructions on setting up GOOGLE_SERVICE_ACCOUNT_KEY_FILE.

from inapppy import GooglePlayVerifier, errors


def consume_purchase(receipt):
    """
    Consume a purchase after validation.
    """
    purchase_token = receipt['purchaseToken']
    product_sku = receipt['productId']

    # Use the service account credentials (see section 4 for setup)
    verifier = GooglePlayVerifier(
        GOOGLE_BUNDLE_ID,
        GOOGLE_SERVICE_ACCOUNT_KEY_FILE,  # Path to JSON key file or dict
    )

    try:
        # First verify the purchase
        verification_result = verifier.verify(
            purchase_token,
            product_sku,
            is_subscription=False
        )

        # Then consume it to prevent refunds
        consume_result = verifier.consume_product(
            purchase_token,
            product_sku
        )

        return {'success': True, 'consumed': True}
    except errors.GoogleError as exc:
        logging.error('Purchase consumption failed {}'.format(exc))
        return {'success': False, 'error': str(exc)}

Note: Only consumable products (one-time purchases) can be consumed. Subscriptions cannot be consumed.

7. App Store (validates receipt using optional shared-secret against iTunes service)

from inapppy import AppStoreValidator, InAppPyValidationError


bundle_id = 'com.yourcompany.yourapp'
auto_retry_wrong_env_request=False # if True, automatically query sandbox endpoint if
                                   # validation fails on production endpoint
validator = AppStoreValidator(bundle_id, auto_retry_wrong_env_request=auto_retry_wrong_env_request)

try:
    exclude_old_transactions=False # if True, include only the latest renewal transaction
    validation_result = validator.validate('receipt', 'optional-shared-secret', exclude_old_transactions=exclude_old_transactions)
except InAppPyValidationError as ex:
    # handle validation error
    response_from_apple = ex.raw_response  # contains actual response from AppStore service.
    pass

8. App Store Response (validation_result / raw_response) example

{
    "latest_receipt": "MIIbngYJKoZIhvcNAQcCoIIbj...",
    "status": 0,
    "receipt": {
        "download_id": 0,
        "receipt_creation_date_ms": "1486371475000",
        "application_version": "2",
        "app_item_id": 0,
        "receipt_creation_date": "2017-02-06 08:57:55 Etc/GMT",
        "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
        "request_date_pst": "2017-02-06 04:41:09 America/Los_Angeles",
        "original_application_version": "1.0",
        "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
        "request_date_ms": "1486384869996",
        "bundle_id": "com.yourcompany.yourapp",
        "request_date": "2017-02-06 12:41:09 Etc/GMT",
        "original_purchase_date_ms": "1375340400000",
        "in_app": [{
            "purchase_date_ms": "1486371474000",
            "web_order_line_item_id": "1000000034281189",
            "original_purchase_date_ms": "1486371475000",
            "original_purchase_date": "2017-02-06 08:57:55 Etc/GMT",
            "expires_date_pst": "2017-02-06 01:00:54 America/Los_Angeles",
            "original_purchase_date_pst": "2017-02-06 00:57:55 America/Los_Angeles",
            "purchase_date_pst": "2017-02-06 00:57:54 America/Los_Angeles",
            "expires_date_ms": "1486371654000",
            "expires_date": "2017-02-06 09:00:54 Etc/GMT",
            "original_transaction_id": "1000000271014363",
            "purchase_date": "2017-02-06 08:57:54 Etc/GMT",
            "quantity": "1",
            "is_trial_period": "false",
            "product_id": "com.yourcompany.yourapp",
            "transaction_id": "1000000271014363"
        }],
        "version_external_identifier": 0,
        "receipt_creation_date_pst": "2017-02-06 00:57:55 America/Los_Angeles",
        "adam_id": 0,
        "receipt_type": "ProductionSandbox"
    },
    "latest_receipt_info": [{
            "purchase_date_ms": "1486371474000",
            "web_order_line_item_id": "1000000034281189",
            "original_purchase_date_ms": "1486371475000",
            "original_purchase_date": "2017-02-06 08:57:55 Etc/GMT",
            "expires_date_pst": "2017-02-06 01:00:54 America/Los_Angeles",
            "original_purchase_date_pst": "2017-02-06 00:57:55 America/Los_Angeles",
            "purchase_date_pst": "2017-02-06 00:57:54 America/Los_Angeles",
            "expires_date_ms": "1486371654000",
            "expires_date": "2017-02-06 09:00:54 Etc/GMT",
            "original_transaction_id": "1000000271014363",
            "purchase_date": "2017-02-06 08:57:54 Etc/GMT",
            "quantity": "1",
            "is_trial_period": "true",
            "product_id": "com.yourcompany.yourapp",
            "transaction_id": "1000000271014363"
        }, {
            "purchase_date_ms": "1486371719000",
            "web_order_line_item_id": "1000000034281190",
            "original_purchase_date_ms": "1486371720000",
            "original_purchase_date": "2017-02-06 09:02:00 Etc/GMT",
            "expires_date_pst": "2017-02-06 01:06:59 America/Los_Angeles",
            "original_purchase_date_pst": "2017-02-06 01:02:00 America/Los_Angeles",
            "purchase_date_pst": "2017-02-06 01:01:59 America/Los_Angeles",
            "expires_date_ms": "1486372019000",
            "expires_date": "2017-02-06 09:06:59 Etc/GMT",
            "original_transaction_id": "1000000271014363",
            "purchase_date": "2017-02-06 09:01:59 Etc/GMT",
            "quantity": "1",
            "is_trial_period": "false",
            "product_id": "com.yourcompany.yourapp",
            "transaction_id": "1000000271016119"
        }],
    "environment": "Sandbox"
}

9. App Store, asyncio version (available in the inapppy.asyncio package)

from inapppy import InAppPyValidationError
from inapppy.asyncio import AppStoreValidator


bundle_id = 'com.yourcompany.yourapp'
auto_retry_wrong_env_request=False # if True, automatically query sandbox endpoint if
                                   # validation fails on production endpoint
validator = AppStoreValidator(bundle_id, auto_retry_wrong_env_request=auto_retry_wrong_env_request)

try:
    exclude_old_transactions=False # if True, include only the latest renewal transaction
    async with validator:  # Use async context manager to ensure proper session management
        validation_result = await validator.validate('receipt', 'optional-shared-secret', exclude_old_transactions=exclude_old_transactions)
except InAppPyValidationError as ex:
    # handle validation error
    response_from_apple = ex.raw_response  # contains actual response from AppStore service.
    pass

10. Development

Prerequisites

Install uv for fast Python package management:

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

Setup and Testing

# Install development dependencies
make dev

# Run linting checks
make lint

# Format code with ruff
make format

# Run both lint and format checks
make check

# Run tests
make test

# Run tests with pytest directly
uv run pytest -v

Available Make Commands

make setup     # Install dependencies with uv
make dev       # Install development dependencies with uv
make clean     # Remove build artifacts
make build     # Build distribution packages
make release   # Upload to PyPI
make test      # Run tests with pytest
make lint      # Run ruff linting
make format    # Format code with ruff
make check     # Run lint and format check
make install   # Install package in editable mode

11. Donate

You can support development of this project by buying me a coffee ;)

Coin Wallet
EUR https://paypal.me/LukasSalkauskas
DOGE DGjSG3T6g9h2k6iSku7mtKCynCpmwowpyN
BTC 1LZAiWmLYzZae4hq3ai9hFYD3e3qcwjDsU
ETH 0xD62245986345130edE10e4b545fF577Bd5BaE3E4

About

Python In-app purchase validator for Apple AppStore and GooglePlay.

Topics

Resources

Code of conduct

Security policy

Stars

218 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages