Skip to content

Implement PG payments

To sell consumable products with PG payments and deliver them to users, complete the following procedure in order.

Before you begin, complete the common prerequisites.

Overall flow

In PG payments, the user pays in a browser outside the app. The app cannot tell whether the payment is complete, so after the user returns, it queries the Hive Axyl server to check the result. In step 10, the app server handles receipt verification and product delivery with the Hive Axyl Server API.

Step Category What you do
1 Store console Get credentials from the PG provider
2 Hive Console Configure PG settings and price tiers, and register products
3 Hive Axyl SDK Install and register the SDK modules
4 Recipe code Copy the recipe folders
5 Hive Axyl SDK Get the product list
6 Recipe code Create a PurchaseOrder
7 Recipe code Issue the payment page URL
8 App code Open the payment page in a browser
9 Recipe code Retrieve the receipt and prepare purchase information
10 App code, App server, Hive Axyl Server API Verify receipts and deliver products
11 Recipe code Finish the transaction
12 Hive Axyl SDK Record delivery results
13 Hive Console, Hive Axyl Server API Check the purchase history
Finish the transaction only after product delivery is complete

Finishing the transaction in step 11 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. Get PG provider credentials

Get credentials from the PG provider you will use. The required values differ by PG provider.

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 price tiers and products.

Setting Required Where to check
Set PG provider credentials and payment methods Required Set PG provider credentials and payment methods
Set price tiers Required Register a price tier
Register products Required Register product information

PG payments charge the price registered in the Hive Console. If you skip setting price tiers, the payment amount is not determined.

3. Prepare SDK modules

Install the required SDK modules, and then register them when you initialize the SDK. PG payments do not use a store plugin. 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 features
    • com.com2usplatform.hiveaxyl.auth: Login session and token issuance for the user who is the payer
    • com.com2usplatform.hiveaxyl.payments: Payment server calls

The following example code registers the authentication, token, and payment modules 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;

var config = CoreConfig.CreateBuilder("{appId}").Build();

HiveBootstrap.Initialize(config, builder =>
{
    builder.AddAuth();
    builder.AddToken();
    builder.AddPayments(sandbox: true);
});

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.Pg/: Code for PG

To call recipes from your app code, add Hive.Axyl.Samples.Recipes, Hive.Axyl.Samples.Recipes.Payments, and Hive.Axyl.Samples.Recipes.Payments.Pg to references in your app's assembly definition.

Platforms that provide PG payments

A PG order must specify the OS that the app client is running on with the OrderRequestOs enum. The recipe fills in Windows, Macos, Android, and Ios on Windows, macOS, Android, and iOS, respectively. On a platform that has no matching value, it cannot create the order and returns a FailedPrecondition error.

5. Get the product list

There is no recipe for getting the product list. Get it directly with FetchPgProductsAsync() of the Hive Axyl SDK.

Put ProductId, Price, and Currency from the result as is into the PurchaseOrder in step 6. 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,
    currency:      product.Currency,
    country:       "KR",
    language:      "ko",
    storePlayerId: 0L,                    // Not used by PG
    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. PG has no market account token to compare against, so the comparison result in the verification response always comes back as 9. However, this value is saved in the payment history and serves as the basis for confirming which user made the payment. 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. Issue the payment page URL

  • Recipe code  Call the recipe code from the app.

When the user selects the payment button, call the recipe's InitiatePurchaseAsync(). It records the purchase intent on the server and returns the payment page URL. Because the user must be sent outside the app, the success status at this point is AwaitingExternal, not Success.

To sell multiple units at once, specify the quantity with new PgPurchaseSource(quantity). If you omit it, the quantity is treated as 1.

7.1. Start the purchase

started.Pending contains the order information, and you receive the receipt in step 9. As in the following example code, save this value to the app's purchase information store first, before checking the result status. Once you save it, you do not lose the order information of this purchase even if a later step fails.

var recipe = new ConsumablePurchaseRecipe(new PgPurchaseSource());

InitiatePurchaseOutcome started = await recipe.InitiatePurchaseAsync(order, token);

if (started.Pending != null)
{
    // Save it first, regardless of the result status.
    await pendingStore.SaveAsync(started.Pending);
}

if (started.Status == InitiatePurchaseStatus.UserCanceled)
{
    // No order was created. Return to the in-app store.
    return;
}

if (started.Status != InitiatePurchaseStatus.AwaitingExternal)
{
    Debug.LogError($"{started.FailedStep}: {started.BusinessOutcome} {started.Error?.Message}");
    return;
}

string payUrl = started.ExternalUrl;   // Open it in step 8
var pending = started.Pending;         // No receipt yet

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
Hive Axyl SDK Issue the payment page URL with CreatePaymentUrlAsync() Create the payment page URL

7.2. Manage the purchase information store

pendingStore in the example code is a store that the app creates, and it must remain even when the app is relaunched. started.Pending in this step does not have an order number yet, so save it with the account and product ID as the key. When you receive prepared.Pending containing the order number in step 9, delete this record and save it again with OrderId as the key. Delete it only after the transaction is finished successfully, and do not write receipt and token values to logs. In the usage example, SavePendingAsync() and MarkClosedAsync() of IPurchaseApplication play the same role.

8. Open the payment page in a browser

Open the URL returned in step 7 in a browser. The recipe only returns the URL and does not open it, because the app decides whether to use an in-app browser or an external browser.

When the user finishes the payment and returns to the app, proceed to step 9. In the usage example, IPurchaseApplication.WaitUntilVerifiableAsync() opens the payment page and waits for the user to return.

The user's return does not mean the payment succeeded

The app cannot know what happened in the browser. You can find orders whose payment is complete only by querying the Hive Axyl server in step 9, and the app server decides whether to deliver the product after verifying the receipt in step 10. Do not deliver the product just because the user returned.

9. Retrieve the receipt and prepare purchase information

  • Recipe code  Call the recipe code from the app.

When the user returns, call PreparePurchaseAsync(). The recipe retrieves the receipt of the order whose payment is complete from the Hive Axyl server, records the purchase result, and returns prepared.Pending filled with the receipt and the order number. 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(pending, token);

if (prepared.Pending != null)
{
    // Replace the saved purchase information with the returned value.
    // If the receipt was retrieved, delete the record from step 7 and save it with OrderId as the key.
    await pendingStore.SaveAsync(prepared.Pending);
}

if (prepared.Status == PreparePurchaseStatus.BusinessOutcome
    && prepared.BusinessOutcome == PurchaseBusinessOutcome.NothingToRestore)
{
    // The user has not finished the payment yet, or the order is already closed. This is not an error.
    // Do not have the app query repeatedly; make this call again when the user requests it again.
    return;
}

if (prepared.Status != PreparePurchaseStatus.Success)
{
    // Do not delete the saved purchase information. Finish it in undelivered purchase restoration.
    return;
}

// Pass prepared.Pending, not pending from step 7, to steps 10 and 11.

The recipe performs the following tasks internally.

Category Call Where to check
Hive Axyl SDK Retrieve the receipt from undelivered orders with RestorePurchasesAsync() Get completed payment information
Hive Axyl SDK Save the purchase result with RecordStorePurchaseAsync() Save payment result data

10. 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 9 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.

10.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 in requestType of the receipt verification request, as is
    • Usage example: Passes PurchaseVerificationKind.NewPurchase or PurchaseVerificationKind.Recovery, and the app server integration code converts these values to 1 and 2, respectively
  • Return value: new PurchaseDeliveryConfirmation() if the app server responds with delivery completed; null if delivery could not be confirmed because of a rejection or failure
  • Keep hiveAxylTransactionId: PurchaseDeliveryConfirmation has 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 12
// 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 12.

10.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: PG
  • axylReceipt: The receipt issued by the Hive Axyl server, contained in VerifyToken. The value retrieved in step 9
  • productId, price, currency, country, language, serverId, iapPayload, accountUuid, appVersion: The values of the same names in Order
  • orderId: OrderId
  • storeTransactionId: StoreTransactionId
  • requestType: 1, which indicates a new purchase

10.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.

  1. Check that hiveAxylProductId, hiveAxylQuantity, hiveAxylPrice, and hiveAxylCurrency in the verification response match the product, quantity, and amount sold to the user. If hiveAxylPurchaseCancelState is 1, the payment was canceled, so do not deliver the product.
  2. Check the app server's delivery records to see whether the purchase has already been delivered. If hiveAxylDuplicated is true, you must check this. If the purchase has already been delivered, skip item 3.
  3. Deliver the product and record delivery completion.
  4. 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 11. Also return hiveAxylTransactionId from the verification response so that it can be used to record the delivery result in step 12.

Determine the delivery target with hiveAxylPayload in the verification response. It is the iapPayload passed 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.

11. 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 10, the recipe does not finish the transaction and returns Failure.

ClosePurchaseOutcome closed = await recipe.ClosePurchaseAsync(
    prepared.Pending,   // Value returned by step 9. You cannot finish the transaction with the Pending from step 7
    delivery,           // Value created in step 10 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.
}

In PG payments, the transaction is finished on the Hive Axyl server, not on the device. The recipe performs the following tasks internally.

Category Call Where to check
Hive Axyl SDK Request purchase finalization from the server with FinalizePurchaseAsync() Finalize the purchase

12. Record 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 10.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 10.1 and kept separately
    Status = 1,   // 1: Delivery succeeded
    Assets = new[]
    {
        new ItemResultAsset { AssetId = "gold", AssetName = "골드", Quantity = 100 },
    },
});

13. 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
AwaitingExternal The payment page URL is ready. This is the success status of starting a purchase in PG payments. Open the browser.
Success The step finished normally. Proceed to the next step.
BusinessOutcome The Hive Axyl server rejected the request, or there is no order to retrieve. 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 SDK reported the payment as canceled. This is not an error, so return to the in-app store.

AwaitingExternal and UserCanceled exist only in the step that starts the purchase. The result types of the prepare and close steps do not have them, so branching on those values does not compile.

Do not discard PendingPurchase even if it fails

PreparePurchaseOutcome and ClosePurchaseOutcome return Pending even when they fail. It is either the Pending you passed or a Pending newly filled with the values retrieved in step 9, so 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 10, 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 PG purchases.