Implement Google Play payments
To sell consumable products on Google Play and deliver them to users, complete the following procedure in order.
Before you begin, complete the common prerequisites.
Overall flow
In step 9, the app server handles receipt verification and product delivery with the Hive Axyl Server API. What you set up and call in each step is as follows.
| Step | Category | What you do |
|---|---|---|
| 1 | Store console | Issue a license key and a service account key in the Google Play Console |
| 2 | Hive Console | Register credentials and products |
| 3 | Hive Axyl SDK, Store plugin | Install and register the SDK modules and the Google payment plugin |
| 4 | Recipe code | Copy the recipe folders |
| 5 | Hive Axyl SDK, Store plugin | Get the product list |
| 6 | Recipe code | Create a PurchaseOrder |
| 7 | Recipe code | Start the purchase |
| 8 | Recipe code | Prepare purchase information |
| 9 | App code, App server, Hive Axyl Server API | Verify receipts and deliver products |
| 10 | Recipe code | Finish the transaction |
| 11 | Hive Axyl SDK | Record delivery results |
| 12 | Hive Console, Hive Axyl Server API | Check the purchase history |
Finish the transaction only after product delivery is complete
Finishing the transaction in step 10 marks this purchase as finished. Restoring undelivered purchases finds only purchases that are not finished, so if you finish the transaction before delivery and the app stops midway, you cannot find that purchase again.
1. Configure the Google Play Console
-
Store console Configure this in the Google Play Console.
Detailed procedure: Google Play Console payment integration
Get a license key and a service account key JSON file from the Google Play Console. The Hive Axyl server uses these values to request receipt verification from Google Play.
Also register the in-app products to sell in the Google Play Console. You use the product IDs registered here as is in step 2.
2. Configure the Hive Console
- Hive Console Configure or check this in the Hive Console.
Register the values you got in step 1 in the Hive Console, and register the products to sell.
| Setting | Required | Where to check |
|---|---|---|
| Register the license key and the service account key | Required | Google Play credentials |
| Register products and enter prices | Required | Register product information |
Match the product IDs in both consoles
The product IDs registered in the Google Play Console and in the Hive Console must be the same. If the values differ, receipt verification fails.
In Google Play payments, Google Play manages the actual amount charged. You also enter a price on the product registration screen of the Hive Console, but this value is used only for checking amounts in the console.
3. Prepare SDK modules and plugins
-
Hive Axyl SDK Call the Hive Axyl SDK from the app.
Store plugin Call the store payment plugin from the app.
Detailed procedure: Install the module, initialize, and log in, Register and retrieve the plugin
Install the required SDK modules and the Google payment plugin, and then register them when you initialize the SDK. Payment works on the basis of the logged-in user, so along with the payment module, you must also register the authentication module AddAuth() and the token module AddToken(). If you do not register the payment module, the recipe fails with a FailedPrecondition error.
- Packages to install
com.com2usplatform.hiveaxyl.core: SDK initialization and common featurescom.com2usplatform.hiveaxyl.auth: Login session and token issuance for the user who is the payercom.com2usplatform.hiveaxyl.payments: Payment server callscom.com2usplatform.hiveaxyl.payments.addon.google: Opening the Google Play payment window
The following example code registers the authentication, token, and payment modules and the Google payment plugin when initializing the SDK. The example connects the payment module to the sandbox server for development and testing, so in production builds, register it with AddPayments() without sandbox: true.
using Hive.Axyl.Core;
using Hive.Axyl.Core.Unity;
using Hive.Axyl.Auth;
using Hive.Axyl.Payments;
using Hive.Axyl.Payments.Addon.Google;
var config = CoreConfig.CreateBuilder("{appId}").Build();
HiveBootstrap.Initialize(config, builder =>
{
builder.AddAuth();
builder.AddToken();
builder.AddPayments(sandbox: true);
builder.AddPlayBilling();
});
4. Install the recipe code
- Recipe code Copy the recipe code into your project.
A recipe is source code that you copy into your project instead of installing as a package. Copy the following items from the axyl-samples-unity repository to Assets/Recipes/ in your Unity project.
- Items to copy
- Recipes.asmdef: Common assembly definition
- AssemblyInfo.cs: Setting that exposes internal helpers to other recipe assemblies
- Helper/: Common code shared by multiple recipes
- Payments/: Recipe body and common types
- Payments.Google/: Code for Google Play
To call recipes from your app code, add Hive.Axyl.Samples.Recipes, Hive.Axyl.Samples.Recipes.Payments, and Hive.Axyl.Samples.Recipes.Payments.Google to references in your app's assembly definition.
Excluded from the build if the Google payment plugin is missing
The assembly definition of Payments.Google/ compiles only when the Google payment plugin package is installed. Even if you do not install the plugin, no compile error occurs; only that assembly is excluded from the build.
5. Get the product list
-
Hive Axyl SDK Call the Hive Axyl SDK from the app.
Store plugin Call the store payment plugin from the app.
There is no recipe for getting the product list. Because you must combine the product IDs registered in the Hive Console with the price information that Google Play holds, make the following calls directly in order.
| Step | Category | Call | Where to check |
|---|---|---|---|
| 1 | Hive Axyl SDK | Get the product IDs registered on the server with ListStoreProductIdsAsync() | Get the product ID list |
| 2 | Store plugin | Connect BillingClient with StartConnectionAsync() | BillingClient connection |
| 3 | Store plugin | Get prices from Play with QueryProductDetailsAsync() | Get Google Play product details |
| 4 | Hive Axyl SDK | Have the server merge the two results with FetchGoogleProductsAsync() | Get product details from the Hive Axyl server |
Put ProductId, Price, and Currency from the merged result as is into the PurchaseOrder in step 6. If you parse the displayed string again or hard-code the values, the price the user saw and the payment request amount differ. A product whose Price is empty is a product whose price could not be confirmed, so do not sell it.
6. Create a PurchaseOrder
- Recipe code Call the recipe code from the app.
PurchaseOrder is a recipe type that holds which product the user wants to purchase and at what price, and it is the starting point of every payment recipe.
using Hive.Axyl.Samples.Recipes;
// Namespace UUID that the app sets once and never changes
const string k_Namespace = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
AccountUuid.TryCompute(k_Namespace, playerId, out var accountUuid);
if (product.Price == null)
{
// Do not sell products whose price could not be confirmed.
return;
}
var order = new PurchaseOrder(
productId: product.ProductId, // Value retrieved in step 5
price: product.Price.Value, // Price displayed on the screen
currency: product.Currency,
country: "KR",
language: "ko",
storePlayerId: 0L, // Not used by Google Play
serverId: "server01",
iapPayload: "{\"character\":\"A\"}",
appVersion: Application.version,
accountUuid: accountUuid);
AccountUuid.TryCompute() is a recipe helper that creates a UUIDv5 from the logged-in user's playerId. The recipe passes this value to Google Play as ObfuscatedAccountId, and the Hive Axyl server compares the two values when it verifies the receipt. For the generation rules, see Generate an AccountUuid.
Put information that identifies the delivery target in iapPayload. This value comes back as is in the verification response, so it becomes the basis for deciding what to deliver to whom when you restore the purchase later.
7. Start the purchase
- Recipe code Call the recipe code from the app.
When the user selects the payment button, call the recipe's InitiatePurchaseAsync(). This single method records the purchase intent on the server, opens the Play payment window, and receives the result.
Have the app record the following two items itself before and after starting the purchase. RecordOrderAsync() and pendingStore in the example code are a recording method and a store that the app creates. Saving to and deleting from pendingStore is the role that SavePendingAsync() and MarkClosedAsync() play in IPurchaseApplication of the usage example. Do not write receipt and token values to logs.
| Record | When to record | Key | Purpose |
|---|---|---|---|
| Order record | Before calling InitiatePurchaseAsync() | Account and product ID | Deferred payments such as convenience store top-ups end without started.Pending and complete later, so when you restore such purchases, this record fills in the amount at the time of purchase. Record it on the app server so that it remains even if the app is reinstalled. |
| Purchase information | Each time you receive Pending, before checking the result status | StoreTransactionId | Even if verification or finishing fails, you do not lose the receipt and order information of this purchase. It must remain even when the app is relaunched, and you delete it only after the transaction is finished successfully. |
var recipe = new ConsumablePurchaseRecipe(new GooglePurchaseSource());
// Before starting the purchase, record the order information at the time of purchase.
await RecordOrderAsync(order);
InitiatePurchaseOutcome started = await recipe.InitiatePurchaseAsync(order, token);
if (started.Pending != null)
{
// Save it first, regardless of the result status.
await pendingStore.SaveAsync(started.Pending);
}
switch (started.Status)
{
case InitiatePurchaseStatus.Success:
// Proceed to step 8.
break;
case InitiatePurchaseStatus.UserCanceled:
// Only the payment window was closed. Nothing was charged.
break;
case InitiatePurchaseStatus.BusinessOutcome
when started.BusinessOutcome == PurchaseBusinessOutcome.StorePurchasePending:
// The payment is not complete yet. Do not deliver the product.
// When the payment completes, handle it in undelivered purchase restoration.
break;
case InitiatePurchaseStatus.BusinessOutcome
when started.BusinessOutcome == PurchaseBusinessOutcome.StoreItemAlreadyOwned:
// A previous purchase was not consumed. Run undelivered purchase restoration first.
break;
default:
Debug.LogError($"{started.FailedStep}: {started.BusinessOutcome} {started.Error?.Message}");
break;
}
The recipe performs the following tasks internally.
| Category | Call | Where to check |
|---|---|---|
| Hive Axyl SDK | Record the purchase intent on the server with CreatePrePurchaseAsync() | Save pre-purchase information |
| Store plugin | Connect BillingClient with StartConnectionAsync() | BillingClient connection |
| Store plugin | Get the product to pay for again with QueryProductDetailsAsync() | Get Google Play product details |
| Store plugin | Subscribe to the PurchasesUpdated event | Subscribe to the purchase result event |
| Store plugin | Open the payment window and receive the result with LaunchBillingFlowAsync() | Open the payment window |
Because the purchase result can arrive the moment the payment window opens, the recipe subscribes to the event first and then opens the payment window.
The Play Billing Library displays in the payment window only the products retrieved in the current connection. So the recipe retrieves the product again before opening the payment window, and this call is not skipped even if you retrieved the product list in step 5.
On success, started.Pending contains a PendingPurchase. Pass this object as is to the next step. If you extract and pass only the receipt string, the purchase token needed to finish the transaction is lost.
8. Prepare purchase information
- Recipe code Call the recipe code from the app.
Call PreparePurchaseAsync() to record the purchase result on the Hive Axyl server and prepare the purchase information to send to the app server. The recipe does not request receipt verification in this step, so even if you receive a success result, you must not deliver the product yet.
PreparePurchaseOutcome prepared = await recipe.PreparePurchaseAsync(started.Pending, token);
if (prepared.Pending != null)
{
// Replace the saved purchase information with the value the prepare step returned.
await pendingStore.SaveAsync(prepared.Pending);
}
if (prepared.Status != PreparePurchaseStatus.Success)
{
// Do not delete the saved purchase information. Finish it in undelivered purchase restoration.
return;
}
// In step 9, send prepared.Pending to the app server.
The recipe performs the following tasks internally.
| Category | Call | Where to check |
|---|---|---|
| Hive Axyl SDK | Save the purchase result with RecordStorePurchaseAsync() | Save payment result data |
9. Verify receipts and deliver products
-
App code Implement this in the app.
App server Implement this on the app server.
Hive Axyl Server API Call the Hive Axyl Server API from the app server.
Detailed procedure: Verify receipts with the Hive Axyl Server API
The app server handles receipt verification and product delivery, and the recipe is not involved in this step. When the app sends prepared.Pending from step 8 to the app server, the app server verifies the receipt with the Hive Axyl Server API, delivers the product only once, and then responds to the app with the result.
9.1. Request verification and delivery from the app server
Call the app server from the app to request receipt verification and product delivery. Send Market, AxylReceipt, VerifyToken, OrderId, and StoreTransactionId of prepared.Pending and the order information in Order to the app server. The app decides how to call the app server and the response format.
VerifyAndGrantOnceAsync() in the example code is the app's method that calls the app server. In the usage example, IPurchaseApplication.VerifyAndGrantOnceAsync() plays the same role. Implement this method as follows.
- Second argument
requestType: A value that indicates whether this is a new purchase or a purchase restoration. The app decides the argument format- Example code: Passes
1, the value to put inrequestTypeof the receipt verification request, as is - Usage example: Passes
PurchaseVerificationKind.NewPurchaseorPurchaseVerificationKind.Recovery, and the app server integration code converts these values to1and2, respectively
- Example code: Passes
- Return value:
new PurchaseDeliveryConfirmation()if the app server responds with delivery completed;nullif delivery could not be confirmed because of a rejection or failure - Keep
hiveAxylTransactionId:PurchaseDeliveryConfirmationhas no field that holds this value, so keep it separately from the app server response and use it to record the delivery result in step 11
// Request receipt verification and product delivery from the app server.
PurchaseDeliveryConfirmation delivery = await VerifyAndGrantOnceAsync(
prepared.Pending,
requestType: 1, // 1: New purchase. PurchaseVerificationKind.NewPurchase in the usage example
token);
if (delivery == null)
{
// The app server did not confirm delivery.
// Keep the saved purchase information and do not finish the transaction.
return;
}
// VerifyAndGrantOnceAsync() keeps hiveAxylTransactionId from the app server response separately.
// Use it as axylTransactionId in step 11.
9.2. Verify receipts with the Hive Axyl Server API
Have the app server call POST /payment/v1/purchase/verify of the Hive Axyl Server API to verify the receipt. Put the App ID registered in the Hive Console in the X-App-Id request header, and for the address to send the request to, see Base URL. Get the access token for the app server that the call requires with Issue a token. For all request and response fields, see Verify consumable product receipts.
Put the PendingPurchase values that the app sent into the request fields as follows.
providerId:GOOGLEaxylReceipt: The Google Play purchase token inVerifyTokenproductId,price,currency,country,language,serverId,iapPayload,accountUuid,appVersion: The values of the same names inOrderorderId:OrderIdstoreTransactionId:StoreTransactionIdrequestType:1, which indicates a new purchase
9.3. Deliver products and respond with delivery completed
After checking the verification response, have the app server deliver the product only once and respond to the app with the result. The app server must do the following.
- Check that
hiveAxylProductId,hiveAxylPrice, andhiveAxylCurrencyin the verification response match the product and amount sold to the user. IfhiveAxylPurchaseCancelStateis1, the payment was canceled, so do not deliver the product. - Check the app server's delivery records to see whether the purchase has already been delivered. If
hiveAxylDuplicatedistrue, you must check this. If the purchase has already been delivered, skip item 3. - Deliver the product and record delivery completion.
- Respond to the app with delivery completed. Respond with delivery completed even for purchases already delivered so that the app finishes the transaction in step 10. Also return
hiveAxylTransactionIdfrom the verification response so that it can be used to record the delivery result in step 11.
The response values you also check when deciding on delivery are as follows.
hiveAxylAccountUuidCompare: The result of comparing the paying account with the requesting account.1is a match,2is a mismatch, and9means the comparison is not possiblehiveAxylPayload: TheiapPayloadpassed in step 6, returned as is
Do not deliver products based only on values the app sent
Have the app server authenticate the requesting user's account directly and check the product and amount with the verification response. The PurchaseDeliveryConfirmation that the app creates is not a value that proves payment. The app server always decides whether to deliver.
10. Finish the transaction
- Recipe code Call the recipe code from the app.
Call ClosePurchaseAsync() after the app server responds with delivery completed. Without the delivery created in step 9, the recipe does not finish the transaction and returns Failure.
ClosePurchaseOutcome closed = await recipe.ClosePurchaseAsync(
prepared.Pending, // Value returned by step 8
delivery, // Value created in step 9 from the app server's delivery completed response
token);
if (closed.Status == ClosePurchaseStatus.Success)
{
// The purchase is complete, so delete the saved purchase information.
await pendingStore.DeleteAsync(prepared.Pending);
}
else
{
// Verification and delivery are already done; only the purchase remains open.
// Keep the saved purchase information and call again, or finish it in undelivered purchase restoration.
}
The recipe performs the following tasks internally.
| Category | Call | Where to check |
|---|---|---|
| Hive Axyl SDK | Confirm the payment with RequestPurchaseAsync() | Request payment confirmation |
| Store plugin | Consume the purchase in Play with ConsumeAsync() | Finish consumable product transactions |
Consuming is not optional
A consumable product that is not consumed cannot be purchased again. Play rejects the purchase with ITEM_ALREADY_OWNED, and after the acknowledgment period passes, Google automatically refunds the purchase.
11. Record delivery results
-
Hive Axyl SDK Call the Hive Axyl SDK from the app.
Detailed procedure: Save product delivery results
After delivery is complete, record the result on the Hive Axyl server. This call does not confirm the payment or decide whether to deliver; it only leaves a record for operations.
In AxylTransactionId, put the hiveAxylTransactionId that VerifyAndGrantOnceAsync() in 9.1 kept separately from the app server response.
using Hive.Axyl.Payments;
var payments = HiveCore.Resolve<IPaymentsService>();
await payments.ItemResultAsync(new ItemResultBody
{
AxylTransactionId = axylTransactionId, // hiveAxylTransactionId received in the app server response in 9.1 and kept separately
Status = 1, // 1: Delivery succeeded
Assets = new[]
{
new ItemResultAsset { AssetId = "gold", AssetName = "골드", Quantity = 100 },
},
});
12. Check the purchase history
-
Hive Console Configure or check this in the Hive Console.
Hive Axyl Server API Call the Hive Axyl Server API from the app server.
During operation, get the purchase history to check the payment status and delivery status.
| Category | Method | Where to check |
|---|---|---|
| Hive Console | Specify search conditions in the console and check | Specify search conditions |
| Hive Axyl Server API | Call the purchase history API from the app server | Get purchase history |
Failure handling
Recipe methods do not throw exceptions; they return result objects. Always check Status first.
Status | Meaning | What the app does |
|---|---|---|
Success | The step finished normally. | Proceed to the next step. |
BusinessOutcome | The Hive Axyl server or the market rejected the request. | Branch on the BusinessOutcome value. For how to handle each value, see Rejection reasons in payment recipes. |
Failure | A technical failure such as a network error or timeout. | Record Error and decide whether to retry. |
UserCanceled | The user closed the payment window. | This is not an error, so return to the in-app store. |
UserCanceled exists only in the step that starts the purchase. The result types of the prepare and close steps do not have it, so branching on that value does not compile.
Do not discard PendingPurchase even if it fails
By the time you receive the receipt, the user has already been charged. PreparePurchaseOutcome and ClosePurchaseOutcome return the Pending they received as is, even when they fail. Replace the purchase information saved in step 7 with the returned Pending so that you can finish that purchase later. Even when the app server could not confirm delivery in step 9, keep the purchase information. Delete the saved purchase information only after the transaction is finished successfully.
Next steps
To handle purchases whose payment is complete but whose delivery is not finished, see Restore undelivered Google Play purchases.