Skip to main content
Version: 1.0

Unified APIs

IAPKit - In-App Purchase Solution

These cross-platform methods work on both iOS and Android. For StoreKit/Play-specific helpers, see the iOS Specific and Android Specific sections.

init_connection()

Initializes the connection to the store. This method must be called before any other store operations.

var iap: GodotIap

func _ready():
if Engine.has_singleton("GodotIap"):
iap = Engine.get_singleton("GodotIap")
_initialize()

func _initialize():
var result_json = iap.init_connection()
var result = JSON.parse_string(result_json)

if result.get("success", false):
print("Store connection initialized")
else:
print("Failed to initialize: ", result.get("error", ""))

Returns: JSON string

{
"success": true
}

end_connection()

Ends the connection to the store and cleans up resources.

func _cleanup():
var result_json = iap.end_connection()
var result = JSON.parse_string(result_json)

if result.get("success", false):
print("Store connection ended")

Returns: JSON string

{
"success": true
}

fetch_products()

Fetches in-app product information from the store.

func load_products():
var product_ids = ["coins_100", "coins_500", "remove_ads"]
var result_json = iap.fetch_products(JSON.stringify(product_ids), "inapp")
var products = JSON.parse_string(result_json)

for product in products:
print("Product: ", product.productId)
print("Price: ", product.localizedPrice)

Parameters:

ParameterTypeDescription
product_idsStringJSON array of product IDs
product_typeStringProduct type: "inapp" or "subs"

Returns: JSON array of products

[
{
"productId": "coins_100",
"title": "100 Coins",
"description": "Get 100 coins",
"price": 0.99,
"localizedPrice": "$0.99",
"currency": "USD"
}
]
Using Signals

You can also use the products_fetched signal to receive products asynchronously:

func _ready():
iap.products_fetched.connect(_on_products_fetched)
iap.fetch_products(JSON.stringify(["coins_100"]), "inapp")

func _on_products_fetched(products: Array):
for product in products:
print("Product: ", product.productId)

fetch_subscriptions()

Fetches subscription information from the store.

func load_subscriptions():
var sub_ids = ["premium_monthly", "premium_yearly"]
var result_json = iap.fetch_subscriptions(JSON.stringify(sub_ids))
var subscriptions = JSON.parse_string(result_json)

for sub in subscriptions:
print("Subscription: ", sub.productId)
print("Price: ", sub.localizedPrice)

Parameters:

ParameterTypeDescription
subscription_idsStringJSON array of subscription IDs

Returns: JSON array of subscriptions

request_purchase()

Initiates a purchase request for a product or subscription.

# Purchase a product
func buy_product(product_id: String):
var params = {
"sku": product_id,
"type": "inapp"
}
var result_json = iap.request_purchase(JSON.stringify(params))
var result = JSON.parse_string(result_json)

if result.get("success", false):
print("Purchase initiated")
# Purchase result comes via purchase_updated signal

# Purchase a subscription
func buy_subscription(subscription_id: String, offer_token: String = ""):
var params = {
"sku": subscription_id,
"type": "subs"
}

# Android: Add offer token if available
if OS.get_name() == "Android" and offer_token != "":
params["offerToken"] = offer_token

iap.request_purchase(JSON.stringify(params))

Parameters (JSON object):

ParameterTypeDescription
skuStringProduct or subscription ID
typeString"inapp" or "subs"
offerTokenString(Android only) Subscription offer token
quantityint(iOS only) Purchase quantity
appAccountTokenString(iOS only) User identifier. Must be UUID format (e.g., 550e8400-e29b-41d4-a716-446655440000). Non-UUID values result in null.

Returns: JSON string with result

Important

request_purchase initiates the purchase flow but does not return the purchase result directly. Handle purchase outcomes through the purchase_updated and purchase_error signals.

finish_transaction()

Completes a purchase transaction. Must be called after successful purchase verification.

func _on_purchase_updated(purchase: Dictionary):
# Validate receipt on your server first
var verified = await verify_on_server(purchase)

if verified:
# Grant purchase to user
grant_purchase_to_user(purchase)

# Finish the transaction
var params = {
"purchase": purchase,
"isConsumable": is_consumable(purchase.productId)
}
var result_json = iap.finish_transaction(JSON.stringify(params))
var result = JSON.parse_string(result_json)

if result.get("success", false):
print("Transaction completed")

Parameters (JSON object):

ParameterTypeDescription
purchaseObjectThe purchase object to finish
isConsumableboolWhether the product is consumable

Returns: JSON string with result

get_available_purchases()

Retrieves available purchases for restoration (non-consumable products and subscriptions).

func restore_purchases():
var purchases_json = iap.get_available_purchases()
var purchases = JSON.parse_string(purchases_json)

for purchase in purchases:
# Validate and restore each purchase
var is_valid = await verify_on_server(purchase)
if is_valid:
grant_purchase_to_user(purchase)

print("Purchases restored")

Returns: JSON array of purchases

[
{
"productId": "remove_ads",
"transactionId": "1000000123456789",
"purchaseToken": "...",
"purchaseState": "purchased"
}
]

Platform behavior:

  • iOS — Returns active entitlements from StoreKit 2
  • Android — Queries both in-app and subscription purchases, merges results

Opens the platform-specific subscription management UI.

func open_subscription_settings():
var options = {}

if OS.get_name() == "Android":
options["packageNameAndroid"] = "com.yourcompany.game"
options["skuAndroid"] = "premium_monthly" # Optional

var result_json = iap.deep_link_to_subscriptions(JSON.stringify(options))
var result = JSON.parse_string(result_json)

if result.get("success", false):
print("Subscription settings opened")

Parameters (JSON object):

ParameterTypeDescription
packageNameAndroidString(Android) Package name
skuAndroidString(Android, optional) Specific subscription SKU

Returns: JSON string with result

verify_purchase()

Verifies a purchase using the native OpenIAP implementation. This validates purchases using platform-specific methods.

func verify(product_id: String, purchase: Dictionary):
var options = {}

if OS.get_name() == "iOS":
options["apple"] = {
"sku": product_id
}
elif OS.get_name() == "Android":
options["google"] = {
"sku": product_id,
"packageName": "com.yourcompany.game",
"purchaseToken": purchase.purchaseToken,
"accessToken": await get_access_token_from_server(),
"isSub": false
}

var result_json = iap.verify_purchase(JSON.stringify(options))
var result = JSON.parse_string(result_json)

if result.get("isValid", false):
print("Purchase verified!")
grant_purchase_to_user(purchase)

Parameters (JSON object):

ParameterTypeDescription
appleObjectiOS verification options
apple.skuStringProduct SKU to validate
googleObjectAndroid verification options
google.skuStringProduct SKU to validate
google.packageNameStringAndroid package name
google.purchaseTokenStringPurchase token from purchase
google.accessTokenStringOAuth2 access token
google.isSubboolWhether product is a subscription

Returns: JSON string with verification result

{
"success": true,
"isValid": true
}

For external verification services with additional security, consider using IAPKit.

Server-Side Verification with IAPKit

For production apps, we recommend using IAPKit for server-side purchase verification. IAPKit provides:

  • Cross-platform verification for both iOS and Android purchases
  • Subscription status tracking with renewal detection
  • Fraud prevention with receipt validation
  • Webhook notifications for real-time purchase events

Basic HTTP Verification

You can call IAPKit's verification API directly from your game or backend server:

# Verify purchase with IAPKit
func verify_with_iapkit(purchase: Dictionary) -> Dictionary:
var http = HTTPRequest.new()
add_child(http)

var headers = [
"Content-Type: application/json",
"Authorization: Bearer YOUR_IAPKIT_API_KEY"
]

var body = {}
if OS.get_name() == "iOS":
body = {
"apple": {
"jws": purchase.get("purchaseToken", "")
}
}
elif OS.get_name() == "Android":
body = {
"google": {
"purchaseToken": purchase.get("purchaseToken", "")
}
}

http.request(
"https://api.iapkit.com/v1/verify",
headers,
HTTPClient.METHOD_POST,
JSON.stringify(body)
)

var response = await http.request_completed
http.queue_free()

var result = JSON.parse_string(response[3].get_string_from_utf8())
return result if result is Dictionary else {}

# Usage in purchase handler
func _on_purchase_updated(purchase: Dictionary):
var state = purchase.get("purchaseState", "")

if state == "purchased":
var verification = await verify_with_iapkit(purchase)

if verification.get("isValid", false):
var iapkit_state = verification.get("state", "")

match iapkit_state:
"entitled":
# User is entitled to the product
grant_purchase(purchase.productId)
"pending":
# Purchase is pending - wait for confirmation
print("Purchase pending")
"expired":
# Subscription has expired
revoke_entitlement(purchase.productId)
"inauthentic":
# Purchase could not be verified - possible fraud
print("Warning: Inauthentic purchase detected")

# Always finish the transaction
var params = {
"purchase": purchase,
"isConsumable": is_consumable(purchase.productId)
}
iap.finish_transaction(JSON.stringify(params))

IAPKit Purchase States

StateDescriptionAction
entitledUser is entitled to the productGrant entitlement
pendingPurchase is pendingWait for confirmation
canceledPurchase was canceledDo not grant
expiredSubscription has expiredRevoke entitlement
ready-to-consumeConsumable ready to be consumedConsume and grant
consumedConsumable has been consumedAlready processed
inauthenticPurchase could not be verifiedReject - possible fraud

See also:


Purchase Interface

The purchase object structure returned from purchases:

# Purchase Dictionary Structure
{
"productId": "coins_100",
"transactionId": "1000000123456789",
"transactionDate": 1703980800000,
"purchaseToken": "...",

# iOS-specific
"originalTransactionDateIOS": 1703980800000,
"originalTransactionIdentifierIOS": "1000000123456789",
"expirationDateIOS": 1706659200000,
"environmentIOS": "Sandbox",

# Android-specific
"purchaseState": "purchased",
"isAcknowledgedAndroid": false,
"autoRenewingAndroid": true,
"packageNameAndroid": "com.yourcompany.game"
}

Complete Example

extends Node

var iap: GodotIap
var products: Array = []

func _ready():
if Engine.has_singleton("GodotIap"):
iap = Engine.get_singleton("GodotIap")
_setup_signals()
_initialize()

func _setup_signals():
iap.purchase_updated.connect(_on_purchase_updated)
iap.purchase_error.connect(_on_purchase_error)
iap.products_fetched.connect(_on_products_fetched)

func _initialize():
var result = JSON.parse_string(iap.init_connection())
if result.get("success", false):
_load_products()

func _load_products():
var product_ids = ["coins_100", "remove_ads"]
iap.fetch_products(JSON.stringify(product_ids), "inapp")

func _on_products_fetched(fetched_products: Array):
products = fetched_products
for product in products:
print("Loaded: ", product.productId, " - ", product.localizedPrice)

func _on_purchase_updated(purchase: Dictionary):
var state = purchase.get("purchaseState", "")

if state == "purchased":
# Verify on server
var verified = await verify_on_server(purchase)
if verified:
grant_purchase(purchase.productId)

# Finish transaction
var params = {
"purchase": purchase,
"isConsumable": is_consumable(purchase.productId)
}
iap.finish_transaction(JSON.stringify(params))

func _on_purchase_error(error: Dictionary):
var code = error.get("code", "")
match code:
"USER_CANCELED":
print("User canceled")
_:
print("Error: ", error.get("message", ""))

func buy(product_id: String):
var params = {"sku": product_id, "type": "inapp"}
iap.request_purchase(JSON.stringify(params))

func restore():
var purchases = JSON.parse_string(iap.get_available_purchases())
for purchase in purchases:
grant_purchase(purchase.productId)