- Introduction
- Installation
- Google Play (receipt + signature)
- Google Play (verification)
- Setting up Google Service Account Credentials
- Usage Example (with file path)
- Usage Example (with credentials dictionary)
- Google Play (verification with result)
- Google Play (consuming products)
- App Store (receipt + using optional shared-secret)
- App Store Response (validation_result / raw_response) example
- App Store, asyncio version (available in the inapppy.asyncio package)
- Development
- Donate
In-app purchase validation library for Apple AppStore and GooglePlay (App Store validator have async support!). Works on python3.6+
pip install inapppy
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
passAn 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": "[email protected]",'
' "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
passBefore 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:
- As a file path (string): Path to the JSON key file downloaded from Google Cloud Console
- As a dictionary (dict): The parsed JSON content of the key file
How to obtain the Service Account Key File:
- 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
- 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
- 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
- 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
- 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
- The JSON key file should contain fields like:
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": "[email protected]",
"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/..."
}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 responseimport 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 responseAlternative 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 resultAfter 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.
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{
"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"
}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.
passInstall 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"# 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 -vmake 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 modeYou can support development of this project by buying me a coffee ;)
| Coin | Wallet |
|---|---|
| EUR | https://paypal.me/LukasSalkauskas |
| DOGE | DGjSG3T6g9h2k6iSku7mtKCynCpmwowpyN |
| BTC | 1LZAiWmLYzZae4hq3ai9hFYD3e3qcwjDsU |
| ETH | 0xD62245986345130edE10e4b545fF577Bd5BaE3E4 |