AndroidIAPP is a plugin for the Godot game engine. It provides an interface to work with Google Play Billing Library version 7.1.1. The plugin supports all public functions of the library, passes all error codes, and can work with different subscription plans.
A simple game to demonstrate the work of purchases and subscriptions with different tariff plans: Circle Catcher 2
- Connect to Google Play Billing.
- Query purchases and product details.
- Make purchases and subscriptions.
- Update, consume, and acknowledge purchases.
-
Install the plugin using Godot Asset Library.
or
-
Download the plugin from GitHub.
-
Place the unpacked plugin folder in the
res://addons/
directory of your project.
Note
Don't forget to enable the plugin in Project > Project Settings > Plugins
.
-
Purchase Types:
- Consumable products (
"inapp"
): Must be consumed usingconsumePurchase
. - Non-consumable products (
"inapp"
): Must be acknowledged usingacknowledgePurchase
. - Subscriptions (
"subs"
): Must be acknowledged usingacknowledgePurchase
. - Product and subscription IDs must be passed as a list of strings, even for a single ID (e.g.,
["product_id"]
).
- Consumable products (
-
Return Format:
- All methods return a Godot
Dictionary
with keys in snake_case (e.g.,getProductId
→product_id
). - See example responses: in-app product, subscription, purchase.
- Error responses include
response_code
(from BillingResponseCode) anddebug_message
.
- All methods return a Godot
-
Godot Compatibility: Tested with Godot 4.5. Kotlin coroutines are not supported in Godot 4.2 or earlier.
- Ensure the plugin is activated in
Project > Project Settings > Plugins
. - Check
AndroidIAPP.gd
to confirm the correct path to the AAR file:if debug: return PackedStringArray(["AndroidIAPP-debug.aar"]) else: return PackedStringArray(["AndroidIAPP-release.aar"])
- Use logcat to check the logs:
./adb logcat | grep IAPP
A full example of how to work with the plugin can be found in billing_example.gd
.
Here is how you can initialize the plugin and connect to its signals:
extends Node
const PLUGIN_NAME: String = "AndroidIAPP"
var billing = null
func _ready() -> void:
if Engine.has_singleton(PLUGIN_NAME):
billing = Engine.get_singleton(PLUGIN_NAME)
# Connect to signals
billing.connected.connect(_on_connected)
billing.disconnected.connect(_on_disconnected)
billing.query_purchases.connect(_on_query_purchases)
billing.query_purchases_error.connect(_on_query_purchases_error)
billing.query_product_details.connect(_on_query_product_details)
billing.query_product_details_error.connect(_on_query_product_details_error)
billing.purchase_updated.connect(_on_purchase_updated)
billing.purchase_cancelled.connect(_on_purchase_cancelled)
billing.purchase_update_error.connect(_on_purchase_update_error)
billing.purchase_consumed.connect(_on_purchase_consumed)
billing.purchase_consumed_error.connect(_on_purchase_consumed_error)
billing.purchase_acknowledged.connect(_on_purchase_acknowledged)
billing.purchase_acknowledged_error.connect(_on_purchase_acknowledged_error)
# Start the connection
if not billing.isReady:
billing.startConnection()
else:
printerr("%s singleton not found" % PLUGIN_NAME)
func _on_connected() -> void:
print("%s: Billing successfully connected" % PLUGIN_NAME)
# Now you can query for products and purchases
billing.queryProductDetails(ITEM_ACKNOWLEDGED, "inapp")
billing.queryPurchases("inapp")
# ... other signal handlers
helloResponse
: Emitted when a response to a hello message is received.- Returns:
String
(the message passed tosayHello
or an error like"Error: Activity is null"
).
- Returns:
startConnection
: Emitted when the connection to Google Play Billing starts.- Returns: None.
connected
: Emitted when successfully connected.- Returns: None.
disconnected
: Emitted when disconnected or if the Android activity is unavailable.- Returns:
Dictionary
(e.g.,{"debug_message": "Activity is null"}
).
- Returns:
query_purchases
: Emitted when a purchase query is successful.- Returns:
Dictionary
response_code
: Integer (e.g.,BillingClient.BillingResponseCode.OK
).purchases_list
: Array of Dictionaries (see purchase example).
- Returns:
query_purchases_error
: Emitted on purchase query errors.- Returns:
Dictionary
response_code
: Integer (e.g.,BillingClient.BillingResponseCode.ERROR
).debug_message
: String (e.g.,"No purchase found"
).purchases_list
: null.
- Returns:
query_product_details
: Emitted when a product details query is successful.- Returns:
Dictionary
response_code
: Integer.product_details_list
: Array of Dictionaries (see in-app example, subscription example).
- Returns:
query_product_details_error
: Emitted on product details query errors.- Returns:
Dictionary
response_code
: Integer.debug_message
: String (e.g.,"No product details found"
).
- Returns:
purchase_updated
: Emitted when purchase information is updated.- Returns:
Dictionary
response_code
: Integer.purchases_list
: Array of Dictionaries (e.g.,{"product_id": "blue_skin_v1", "purchase_token": "...", "is_acknowledged": false}
).
- Returns:
purchase
: Emitted when a purchase flow is successfully initiated.- Returns:
Dictionary
- Returns:
purchase_error
: Emitted on purchase errors.- Returns:
Dictionary
response_code
: Integer.debug_message
: String (e.g.,"Activity is null"
,"Product ID list is empty"
).product_id
: String (if applicable).base_plan_id
: String (for subscriptions).
- Returns:
purchase_cancelled
: Emitted when a purchase is cancelled by the user.- Returns:
Dictionary
response_code
: Integer (e.g.,BillingClient.BillingResponseCode.USER_CANCELED
).debug_message
: String.
- Returns:
purchase_update_error
: Emitted on purchase update errors.- Returns:
Dictionary
response_code
: Integer.debug_message
: String.
- Returns:
purchase_consumed
: Emitted when a purchase is consumed.- Returns:
Dictionary
response_code
: Integer.purchase_token
: String.
- Returns:
purchase_consumed_error
: Emitted on consume errors.- Returns:
Dictionary
response_code
: Integer.debug_message
: String.purchase_token
: String.
- Returns:
purchase_acknowledged
: Emitted when a purchase is acknowledged.- Returns:
Dictionary
response_code
: Integer.purchase_token
: String.
- Returns:
purchase_acknowledged_error
: Emitted on acknowledge errors.- Returns:
Dictionary
response_code
: Integer.debug_message
: String.purchase_token
: String.
- Returns:
billing_info
: An informational signal for debugging.- Returns:
Dictionary
plugin_name
: String.fun_name
: String (e.g.,"sayHello"
,"startConnection"
).debug_message
: String.
- Returns:
price_change_acknowledged
: Not implemented.price_change_error
: Not implemented.in_app_message_result
: Not implemented.alternative_billing_only_transaction_reported
: Not implemented.
startConnection()
: Starts the connection to Google Play Billing.
- Emits:
startConnection
,connected
, ordisconnected
(if activity is unavailable). - Warning: Ensure the plugin is initialized after Godot's Android activity is available.
endConnection()
: Ends the connection to the Google Play Billing service.
isReady
: Checks if the billing connection is ready.
- Returns:
bool
.
sayHello(says: String)
: Sends a test message.
- Emits:
helloResponse
. - Displays a Toast and logs to the console.
- Warning: May fail with
"Error: Activity is null"
if called too early. Avoid in production.
queryPurchases(productType: String)
: Queries purchases.
productType
:"inapp"
or"subs"
.- Emits:
query_purchases
orquery_purchases_error
.
queryProductDetails(listOfProductsIDs: Array<String>, productType: String)
: Queries product or subscription details.
listOfProductsIDs
: List of product/subscription IDs (must not be empty).productType
:"inapp"
or"subs"
.- Emits:
query_product_details
orquery_product_details_error
.
purchase(listOfProductsIDs: Array<String>, isOfferPersonalized: bool)
: Initiates a product purchase.
listOfProductsIDs
: List of product IDs (must not be empty).isOfferPersonalized
: Set tofalse
unless complying with EU directive (see details).- Emits:
purchase_updated
,purchase_error
,purchase_cancelled
,purchase_update_error
, orquery_product_details_error
. - Important: Call
consumePurchase
oracknowledgePurchase
to complete the transaction.
subscribe(listOfProductsIDs: Array<String>, basePlanIDs: Array<String>, isOfferPersonalized: bool)
: Initiates a subscription.
listOfProductsIDs
,basePlanIDs
: Lists of IDs (must not be empty).isOfferPersonalized
: Set tofalse
unless complying with EU directive.- Emits:
purchase_updated
,purchase_error
,purchase_cancelled
,purchase_update_error
, orquery_product_details_error
. - Important: Call
acknowledgePurchase
to complete the subscription.
consumePurchase(purchaseToken: String)
: Consumes a purchase.
purchaseToken
: Token frompurchase_updated
response.- Emits:
purchase_consumed
orpurchase_consumed_error
.
acknowledgePurchase(purchaseToken: String)
: Acknowledges a purchase or subscription.
purchaseToken
: Token frompurchase_updated
response.- Emits:
purchase_acknowledged
orpurchase_acknowledged_error
.
The following functions are included in the plugin as stubs but are not yet implemented:
showInAppMessages()
launchPriceChangeConfirmationFlow(productDetails: Dictionary)
createAlternativeBillingOnlyReportingDetails()
reportAlternativeBillingOnlyTransaction(reportingDetails: Dictionary)
Once connected, you should query for the products you have set up in the Google Play Console.
const ITEM_CONSUMABLE: Array = ["additional_life_v1"]
const ITEM_NON_CONSUMABLE: Array = ["red_skin_v1", "blue_skin_v1"]
const SUBSCRIPTIONS: Array = ["remove_ads_sub_01"]
func _on_connected() -> void:
print("%s: Billing successfully connected" % PLUGIN_NAME)
# Query for different types of products
billing.queryProductDetails(ITEM_CONSUMABLE, "inapp")
billing.queryProductDetails(ITEM_NON_CONSUMABLE, "inapp")
billing.queryProductDetails(SUBSCRIPTIONS, "subs")
func _on_query_product_details(response: Dictionary) -> void:
for product in response["product_details_list"]:
# Store product details to display in your shop UI
G.product_showcase.append(product)
product_showcase_updated.emit()
To start a purchase, call the purchase
or subscribe
function with the appropriate product and base plan IDs.
# For a one-time product (consumable or non-consumable)
func do_purchase(product_id: String):
billing.purchase([product_id], false)
# For a subscription
func do_subscription(subscription_id: String, base_plan_id: String):
billing.subscribe([subscription_id], [base_plan_id], false)
The purchase_updated
signal is the central place to handle all new purchases. You need to determine whether to acknowledge or consume the item.
func _on_purchase_updated(response: Dictionary) -> void:
for purchase in response["purchases_list"]:
process_purchase(purchase)
func process_purchase(purchase: Dictionary) -> void:
for product_id in purchase["products"]:
if purchase["purchase_state"] != 1: # Not PURCHASED
print("Purchase is pending for: %s" % product_id)
return
if product_id in ITEM_NON_CONSUMABLE or product_id in SUBSCRIPTIONS:
# Acknowledge non-consumables and subscriptions
if not purchase["is_acknowledged"]:
billing.acknowledgePurchase(purchase["purchase_token"])
else:
# Grant entitlement
print("Purchase already acknowledged: %s" % product_id)
elif product_id in ITEM_CONSUMABLE:
# Consume consumables
billing.consumePurchase(purchase["purchase_token"])
Listen to the corresponding signals to confirm the transaction is complete and update the user's entitlements.
func _on_purchase_consumed(response: Dictionary) -> void:
print("Purchase consumed: %s" % response["purchase_token"])
# Grant consumable item to the user (e.g., add a life)
G.increase_lives()
func _on_purchase_acknowledged(response: Dictionary) -> void:
print("Purchase acknowledged: %s" % response["purchase_token"])
# Unlock feature or content
G.unlock_skin()
To restore purchases (e.g., when a user reinstalls the app), query for their active purchases.
func _on_connected() -> void:
# ... query for product details ...
# Query for existing purchases
billing.queryPurchases("inapp")
billing.queryPurchases("subs")
func _on_query_purchases(response: Dictionary) -> void:
for purchase in response["purchases_list"]:
process_purchase(purchase) # Use the same processing logic
"Activity is null"
: The Android activity is not available. Ensure the plugin is called after Godot initialization."Product ID list is empty"
: Theproduct_id
orbase_plan_id
list is empty. Always pass non-empty lists."Base Plan ID not found"
: Thebase_plan_id
does not match any subscription plan in Google Play Console."No product details found"
: The product ID or type is invalid. Verify IDs in Google Play Console."Purchase token is blank"
: Thepurchase_token
is empty or invalid. Check thepurchase_updated
response.