Implement Apple App Store payments
To sell consumable products on the Apple App Store 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 authentication keys in App Store Connect |
| 2 | Hive Console | Register credentials and products |
| 3 | Hive Axyl SDK, Store plugin | Install and register the SDK modules and the Apple 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 App Store Connect
-
Store console Configure this in App Store Connect.
Detailed procedure: Apple console payment integration
Get the Private Key, Private Key ID, and Issuer ID from App Store Connect. The Hive Axyl server uses these values to request receipt verification from Apple.
Also register the in-app products to sell in App Store Connect. 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 authentication keys | Required | App Store credentials |
| Register products and enter prices | Required | Register product information |
Match the product IDs in both consoles
The product IDs registered in App Store Connect and in the Hive Console must be the same. If the values differ, receipt verification fails.
In Apple App Store payments, the App Store manages the actual amount charged. You enter the price directly on the product registration screen of the Hive Console, and this value is used 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 the plugin
Install the required SDK modules and the Apple 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.apple: Opening the StoreKit payment window
The following example code registers the authentication, token, and payment modules and the Apple 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.Apple;
var config = CoreConfig.CreateBuilder("{appId}").Build();
HiveBootstrap.Initialize(config, builder =>
{
builder.AddAuth();
builder.AddToken();
builder.AddPayments(sandbox: true);
builder.AddStoreKit();
});
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.Apple/: Code for Apple App Store
To call recipes from your app code, add Hive.Axyl.Samples.Recipes, Hive.Axyl.Samples.Recipes.Payments, and Hive.Axyl.Samples.Recipes.Payments.Apple to references in your app's assembly definition.
Excluded from the build if the Apple payment plugin is missing
The assembly definition of Payments.Apple/ compiles only when the Apple 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 the App Store 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 | Get prices from StoreKit with GetProductsAsync() | Get Apple App Store product information |
| 3 | Hive Axyl SDK | Have the server merge the two results with FetchAppleProductsAsync() | Get product details |
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 Apple App Store
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 StoreKit as AppAccountToken, 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 StoreKit 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 | Payments awaiting parental approval 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 ApplePurchaseSource());
// 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:
// Awaiting parental approval. Do not deliver the product.
// When it is approved, handle it in undelivered purchase restoration.
break;
default:
Debug.LogError($"{started.FailedStep}: {started.BusinessOutcome} {started.Error?.Message}");
break;
}
Apple App Store does not support multi-item purchases. Create ApplePurchaseSource without arguments, as in the example above. For the user to buy multiple units of the same product, the payment must be made separately that many times.
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 | Open the payment window and receive the result with PurchaseAsync() | Request Apple App Store payment |
Even when StoreKit cannot verify the transaction signature, the recipe continues the purchase without stopping and passes the error StoreKit left in StoreVerificationError of started.Pending. The final decision is made by the Hive Axyl server's receipt verification that the app server requests, and this verification rejects receipts that cannot be verified. To decide separately based on your app's policy, check this value before delivering the product.
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 transaction ID 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:APPLEaxylReceipt: The StoreKit transaction JWS 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 | Finish the StoreKit transaction with FinishTransactionAsync() | Finish the transaction |
Finishing the transaction is not optional
StoreKit delivers transactions that are not finished again every time the app launches.
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 Apple App Store purchases.