- Notifications
You must be signed in to change notification settings - Fork50
Python In-app purchase validator for Apple AppStore and GooglePlay.
License
dotpot/InAppPy
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
- 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 haveasync support!). Works on python3.6+
pip install inapppy
frominapppyimportGooglePlayValidator,InAppPyValidationErrorbundle_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 purchasevalidation_result=validator.validate('receipt','signature')exceptInAppPyValidationError:# handle validation errorpass
An additional example showing how to authenticate using dict credentials instead of loading from a file
importjsonfrominapppyimportGooglePlayValidator,InAppPyValidationErrorbundle_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 purchasevalidation_result=validator.validate('receipt','signature')exceptInAppPyValidationError:# handle validation errorpass
Before using Google Play verification, you need to set up a Google Service Account and obtain the credentials file. This section explains whatGOOGLE_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 toGoogle Play Console
- Select your app
- Navigate toSettings → Developer account → API access
- If you haven't linked a project yet, clickLink to create or link a Google Cloud project
- Accept the terms and conditions
- Create a Service Account
- In the API access page, scroll toService accounts
- ClickCreate new service account orLearn how to create service accounts (this will take you to Google Cloud Console)
- In Google Cloud Console:
- Go toIAM & Admin → Service Accounts
- Click+ CREATE SERVICE ACCOUNT
- Enter a name (e.g., "InAppPy Validator") and description
- ClickCREATE AND CONTINUE
- Skip granting roles (not needed for this step)
- ClickDONE
- 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
- ClickGrant access
- UnderApp permissions, select your app
- UnderAccount permissions, enable:
- View financial data (for viewing purchase/subscription info)
- Manage orders and subscriptions (if you need to consume products or manage subscriptions)
- ClickInvite user and thenSend invitation
- Download the JSON Key File
- Go back toGoogle Cloud Console → IAM & Admin → Service Accounts
- Click on your service account email
- Go to theKEYS tab
- ClickADD KEY → Create new key
- SelectJSON as the key type
- ClickCREATE
- 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":"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/..."}frominapppyimportGooglePlayVerifier,errorsdefgoogle_validator(receipt):""" Accepts receipt, validates in Google. """purchase_token=receipt['purchaseToken']product_sku=receipt['productId']# Pass the path to your service account JSON key fileverifier=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']=Trueresponse['transactions'].append( (result['orderId'],product_sku) )excepterrors.GoogleErrorasexc:logging.error('Purchase validation failed {}'.format(exc))returnresponse
importjsonfrominapppyimportGooglePlayVerifier,errorsdefgoogle_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 dictionaryverifier=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']=Trueresponse['transactions'].append( (result['orderId'],product_sku) )excepterrors.GoogleErrorasexc:logging.error('Purchase validation failed {}'.format(exc))returnresponse
Alternative to .verify method, instead of raising an error result class will be returned.
Note: See section 4 for instructions on setting upGOOGLE_SERVICE_ACCOUNT_KEY_FILE.
frominapppyimportGooglePlayVerifier,errorsdefgoogle_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 dataraw_response=result.raw_responseis_canceled=result.is_canceledis_expired=result.is_expiredreturnresult
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 upGOOGLE_SERVICE_ACCOUNT_KEY_FILE.
frominapppyimportGooglePlayVerifier,errorsdefconsume_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 purchaseverification_result=verifier.verify(purchase_token,product_sku,is_subscription=False )# Then consume it to prevent refundsconsume_result=verifier.consume_product(purchase_token,product_sku )return {'success':True,'consumed':True}excepterrors.GoogleErrorasexc: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.
frominapppyimportAppStoreValidator,InAppPyValidationErrorbundle_id='com.yourcompany.yourapp'auto_retry_wrong_env_request=False# if True, automatically query sandbox endpoint if# validation fails on production endpointvalidator=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 transactionvalidation_result=validator.validate('receipt','optional-shared-secret',exclude_old_transactions=exclude_old_transactions)exceptInAppPyValidationErrorasex:# handle validation errorresponse_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"}frominapppyimportInAppPyValidationErrorfrominapppy.asyncioimportAppStoreValidatorbundle_id='com.yourcompany.yourapp'auto_retry_wrong_env_request=False# if True, automatically query sandbox endpoint if# validation fails on production endpointvalidator=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 transactionasyncwithvalidator:# Use async context manager to ensure proper session managementvalidation_result=awaitvalidator.validate('receipt','optional-shared-secret',exclude_old_transactions=exclude_old_transactions)exceptInAppPyValidationErrorasex:# handle validation errorresponse_from_apple=ex.raw_response# contains actual response from AppStore service.pass
Installuv for fast Python package management:
# macOS/Linuxcurl -LsSf https://astral.sh/uv/install.sh| sh# Windowspowershell -c"irm https://astral.sh/uv/install.ps1 | iex"
# Install development dependenciesmake dev# Run linting checksmake lint# Format code with ruffmake format# Run both lint and format checksmake check# Run testsmaketest# Run tests with pytest directlyuv run pytest -v
make setup# Install dependencies with uvmake dev# Install development dependencies with uvmake clean# Remove build artifactsmake build# Build distribution packagesmake release# Upload to PyPImaketest# Run tests with pytestmake lint# Run ruff lintingmake format# Format code with ruffmake check# Run lint and format checkmake install# Install package in editable mode
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
License
Code of conduct
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors15
Uh oh!
There was an error while loading.Please reload this page.