Step 3. Purchase a product
Open the Apple App Store payment window for the product the user selected and proceed with the purchase approval process. As the result of the call, you receive the payment completion signal and the purchase receipt data.
Follow the process below to purchase an Apple product.
1. Display the in-app store UI
Implement the in-app store UI in the app and display it to users. Let users select a product and select the payment button. When the user selects the payment button, proceed to the next step.
2. Save pre-purchase information
After the user selects the payment button and before the store payment window opens, save the purchase attempt information. For consumable products, calling CreatePrePurchaseAsync() in this section is optional. For subscription products, you must call PrepareSubscriptionAsync() instead of CreatePrePurchaseAsync(). Because subscription auto-renewal notifications do not contain the Player ID, the Hive Axyl server finds the Player ID and IapPayload in the record saved with this method. For the request fields, see PrepareSubscriptionAsync, and for the full flow for consumable products, see Record purchase information.
CreatePrePurchaseAsync
CreatePrePurchaseAsync() saves the product, expected payment amount, currency, country and language, app server, and IapPayload as a pre-purchase tracking record.
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | PrePurchase | Required | The request data object for saving pre-purchase information. |
| context | ApiCallContext | Optional | Per-call settings object that specifies the idempotency key, cancellation token, and request policy. If omitted, the default values are used. |
PrePurchase
| Field name | Type | Required | Description |
|---|---|---|---|
Country | string | Required | Pass the user's country code (two-letter ISO 3166-1 code) |
Currency | string | Required | Pass the payment currency (three-letter ISO 4217 code). Use the currency value of the product retrieved in Step 2. Get the product list |
Language | string | Required | Pass the language code (two-letter ISO 639-1 code) |
Price | decimal | Required | The expected payment amount. It may include decimals depending on the currency. Pass the price value of the product retrieved in Step 2. Get the product list |
ProductId | string | Required | The unique identifier of the in-app product the user selected. Pass the Product ID retrieved in Step 2. Get the product list |
ProviderId | PrePurchaseProviderId | Required | The market and payment method identifier (enum). Because this request is for Apple App Store payments only, specify only Apple. |
ServerId | string | Optional | The server ID you check on the App Server tab after you register the app server in App Info > App Server by following Hive Console App Server |
AccountUuid | string | Optional | A UUIDv5 value generated from the logged-in user's playerId. Pass it before the store payment window opens. See Generate an AccountUuid. |
IapPayload | string | Optional | The payload (JSON string) that the developer attaches to the market payment. It is passed as is to the app server callback after the purchase is completed. |
RequestDate | DateTimeOffset | Optional | The request time (UTC). If omitted, the server's current time is used. |
IapPayload usage example
With IapPayload in PrePurchase, you can include app-specific data in the receipt that is issued after the purchase is completed. The following is one example of using IapPayload.
Note
For example, suppose that in a mobile app with three user profiles, A, B, and C, a user purchases an in-app product with profile A. If you request the purchase with a JSON string such as {"character": "A"} in IapPayload, this value is included in the purchase receipt and passed to the app when the payment completes successfully.
If the payment is completed but the product cannot be delivered because of a network error, you must retrieve the failed delivery, verify its receipt again, and then deliver the product. At this point, you need to know which of the user's profiles A, B, and C should receive the product. With the {"character": "A"} information in the IapPayload included in the purchase receipt, you can confirm that profile A is the delivery target and deliver the product accurately.
This is only one example. You can put information that your app needs, such as the product purchase time and purchasing user information, in IapPayload and attach it to the purchase receipt. Use it to fit your app's situation.
Call example
Check the success result of PaymentsCreatePrePurchaseResult and the domain-specific results (Outcome) defined by this method in the example below and in the response status. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling.
using System;
using Hive.Axyl.Payments;
using Hive.Axyl.Core;
// payments: IPaymentsService registered during initialization (for how to retrieve it, see [Install and initialize the module](../init.md))
IPaymentsService payments = HiveCore.Resolve<IPaymentsService>();
// Create the request object with the product information at the time the user selected the payment button
var request = new PrePurchase
{
Country = "KR",
Currency = "KRW",
Language = "ko",
Price = 1200.0m,
ProductId = "com.game.item.gold_100",
ProviderId = PrePurchaseProviderId.Apple,
ServerId = "server01",
RequestDate = DateTimeOffset.UtcNow,
AccountUuid = accountUuid
};
PaymentsCreatePrePurchaseResult result = await payments.CreatePrePurchaseAsync(request);
switch (result)
{
case PaymentsCreatePrePurchaseResult.Success success:
// Pre-save succeeded. No data is returned. Proceed to open the store payment window.
break;
// Problem with request values — check the required values and format
case PaymentsCreatePrePurchaseResult.PaymentBadRequest:
case PaymentsCreatePrePurchaseResult.PaymentInvalidParameter:
Debug.LogError("Check the request parameters.");
break;
case PaymentsCreatePrePurchaseResult.UnknownOutcome unknownOutcome:
Debug.LogWarning($"Unknown result: {unknownOutcome.Code}");
break;
// Handle common Failure
case PaymentsCreatePrePurchaseResult.Failure failure:
HiveError err = failure.Problem;
Debug.LogError($"[{err.Code}] {err.Message} (trace: {err.TraceId})");
break;
// Safety net: unhandled results
default:
Debug.LogWarning($"Unhandled result: {result.GetType().Name}");
break;
}
Response data
No data is returned on success.
Response example
Response status
The following table summarizes the success result of PaymentsCreatePrePurchaseResult and the domain-specific results (Outcome) defined by this method. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling. No data is returned on success.
| Response case | Description | App client handling |
|---|---|---|
Success | Pre-purchase information saved | Proceed to open the store payment window |
PaymentBadRequest | The payment request cannot be processed. | Check the request values and call conditions |
PaymentInvalidParameter | The payment request parameters are invalid. | Check the request field values |
UnknownOutcome | A domain-specific result that the SDK does not recognize. | Treat it as a failure and log the result code |
Failure | Common Failure. See Common error handling. | Handle it according to the common error handling guidelines |
3. Request Apple App Store payment
When the user selects the payment button, open the Apple App Store payment window through the Apple payment plugin. When the user approves the payment, you get the StoreKit 2 transaction JWS (JwsRepresentation) and the market order number. Because the Apple App Store does not support multi-item purchases due to platform policy, users can purchase only one product at a time.
Call PurchaseAsync() of the Apple payment plugin (IAppleStoreKitPlugin) to display the Apple App Store payment window and get the user's payment approval.
PurchaseAsync
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | PurchaseRequest | Required | The purchase request data object. |
| ct | CancellationToken | Optional | The cancellation token. |
PurchaseRequest
| Field name | Type | Required | Description |
|---|---|---|---|
ProductId | string | Required | The unique identifier (Product ID) of the product to purchase. Specify the product ID registered in the Apple App Store. |
Options | PurchaseOptions? | Optional | Purchase options. |
PurchaseOptions
| Field name | Type | Required | Description |
|---|---|---|---|
AppAccountToken | string? | Optional | Pass the same UUID string as the value created in Generate an AccountUuid. Apple associates this value with the transaction. |
Quantity | int? | Optional | The purchase quantity. Because the Apple App Store does not support multi-item purchases, do not specify it. The default value is 1. |
Pass a string in a valid UUID format to AppAccountToken. If you pass a value that is not in UUID format, the payment may proceed without the token.
For subscription products, also pass the same value to SubscriptionPrePurchaseRequest.AccountUuid of PrepareSubscriptionAsync(), which you call in 2. Save pre-purchase information. This value is the key for finding the pre-purchase record during subscription auto-renewal and restoration. For the request fields, see PrepareSubscriptionAsync.
Call example
Check the success result of AppleStoreKitServicePurchaseResult and the domain-specific results (Outcome) defined by this method in the example below and in the response status. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling.
using System;
using Hive.Axyl.Core;
using Hive.Axyl.Payments.Addon.Apple;
using System.Threading;
if (!Guid.TryParse(accountUuid, out _))
{
Debug.LogError("Check that AccountUuid is in UUID format.");
return;
}
// Retrieve the Apple StoreKit plugin (must be registered with builder.AddStoreKit() during initialization)
if (!HiveCore.TryResolve<IAppleStoreKitPlugin>(out var applePlugin))
{
Debug.LogError("The Apple StoreKit plugin is not registered.");
return;
}
// Create the purchase request object
var request = new PurchaseRequest
{
ProductId = "com.game.item.gold_100",
Options = new PurchaseOptions
{
AppAccountToken = accountUuid
}
};
AppleStoreKitServicePurchaseResult result = await applePlugin.PurchaseAsync(request, CancellationToken.None);
switch (result)
{
case AppleStoreKitServicePurchaseResult.Success success:
// Payment succeeded. Proceed to the next step with the transaction information
AppleTransaction tx = success.Data.Transaction;
string receipt = tx.JwsRepresentation; // JWS receipt for server verification
ulong transactionId = tx.Id; // Market transaction ID
Debug.Log($"Purchase succeeded — product: {tx.ProductId}, transaction ID: {tx.Id}");
break;
case AppleStoreKitServicePurchaseResult.UserCanceled:
// When the user canceled the payment
Debug.Log("The user canceled the payment.");
break;
case AppleStoreKitServicePurchaseResult.Pending:
// Awaiting parental approval, such as Ask-to-Buy. Receive the result later through the TransactionUpdated event
Debug.Log("The payment is awaiting approval (Ask-to-Buy).");
break;
case AppleStoreKitServicePurchaseResult.UnknownOutcome:
// Unknown result
Debug.LogWarning("Unknown payment result.");
break;
case AppleStoreKitServicePurchaseResult.Failure failure:
// Network or store error
Debug.LogError($"Payment failed: {failure}");
break;
default:
Debug.LogWarning($"Unhandled result: {result.GetType().Name}");
break;
}
Response data
On success, the result is contained in Data (PurchaseResponse) of AppleStoreKitServicePurchaseResult.Success.
| Field name | Type | Description |
|---|---|---|
Data.Transaction | AppleTransaction | The Apple App Store transaction information. |
AppleTransaction
| Field name | Type | Description |
|---|---|---|
Id | uint64 | The unique ID of the transaction. Use this value when you finish the transaction (FinishTransactionAsync). |
OriginalId | uint64 | The transaction ID of the original purchase. Used to track the original transaction when a subscription renews. |
ProductId | string | The unique identifier of the purchased product. |
ProductType | AppleProductType | The product type. The members are Consumable, NonConsumable, AutoRenewable, and NonRenewable. |
PurchaseDate | DateTimeOffset | The purchase time. |
JwsRepresentation | string | The receipt string in JWS (JSON Web Signature) format. Pass this value as AxylReceipt for server verification. |
VerificationStatus | AppleVerificationStatus | The result of StoreKit verifying the receipt signature. Verified or Unverified. |
Quantity | int | The purchase quantity. |
Response status
The returned object AppleStoreKitServicePurchaseResult branches into one of the following cases. Handle them with a switch statement.
| Response case | Description | App client handling |
|---|---|---|
Success | Payment succeeded. Data.Transaction contains the transaction information. | Save the receipt (JwsRepresentation) and the transaction ID (Id), and proceed to the next step |
UserCanceled | The user canceled the payment | Display a payment cancellation notice UI |
Pending | Awaiting parental approval (Ask-to-Buy) | Display an approval pending notice UI. Then receive the result through the TransactionUpdated event |
UnknownOutcome | Unknown result | Retry or show an error message |
Failure | Common Failure. See Common error handling. | Handle it according to the common error handling guidelines |
Ask-to-Buy (awaiting parental approval)
The Pending result is the state of waiting for parental approval when a minor in Family Sharing tries to make a purchase. When the parent approves or declines, you receive the result through the TransactionUpdated event. The app must inform the user of the Pending state and proceed with follow-up processing through the event.
Call the Apple payment plugin, and after the payment is approved, receive and save the following values.
- StoreKit 2 transaction JWS (
JwsRepresentation) - Market order number
4. Save payment result data
For consumable products, save the receipt and transaction information right after the payment is completed. Because this keeps a record of the payment before receipt verification, you can use it for verification, product delivery, customer support, and settlement even if later processing is interrupted. Complete this step before receipt verification. For subscription products, proceed with 5. Save subscription purchase information instead of this step. For the full flow, see Record purchase information.
RecordStorePurchaseAsync
RecordStorePurchaseAsync() saves payment results, such as the receipt and transaction ID, to the Hive Axyl server.
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | PurchaseRequest | Required | The request data object for saving the payment result. |
| context | ApiCallContext | Optional | Per-call settings object that specifies the idempotency key, cancellation token, and request policy. If omitted, the default values are used. |
PurchaseRequest
| Field name | Type | Required | Description |
|---|---|---|---|
AccountUuid | string | Optional | A UUIDv5 value generated from the logged-in user's playerId. Pass it when you record the purchase result after the store payment ends. If you passed it at the payment start step, use the same value. See Generate an AccountUuid. |
AxylReceipt | string | Required | The StoreKit 2 transaction JWS (JwsRepresentation) received from the Apple App Store. |
Country | string | Required | The country code (two-letter ISO 3166-1 code). Example: KR |
Currency | string | Optional | The payment currency (three-letter ISO 4217 code). Paired with Price, it is compared with the values that the server recorded. If you send only one of the two, the currency is not compared and only the amount is compared. |
IapPayload | string | Optional | The payload (JSON string) that the developer attached to the market payment. It is passed as is to app server verification and to the purchase completion callback. |
Language | string | Required | The language code (two-letter ISO 639-1 code). Example: ko |
OrderId | string | Optional | The Hive Axyl internal order number. Used for Steam and PG. |
Price | decimal | Optional | The payment amount. It may include decimals depending on the currency. If you send it, the server compares it with the amount it recorded. If you omit it, no comparison is made. For Apple payments, the request is rejected if the amounts differ. The amount recorded in the payment history is the value that the server confirmed with the market. |
ProductId | string | Required | The in-app product ID registered in the market. |
ProjectInfo | string | Optional | A free-form JSON string for each app. The value is only stored and is not passed to the app server. Set the values to pass to the app server in IapPayload. |
ProviderId | PurchaseRequestProviderId | Required | Because this request is only for saving Apple payment results, specify Apple. |
Quantity | int | Optional | The purchase quantity |
RequestDate | DateTimeOffset | Optional | The client request time (UTC). If omitted, the server's current time is used. |
RequestType | int | Optional | The request type. 1: new purchase, 2: purchase restoration |
ServerId | string | Optional | The ID of the app server registered by following Hive Console App Server. |
StoreTransactionId | string | Optional | The store transaction ID on the market side. |
Call example
Check the success result of PaymentsRecordStorePurchaseResult and the domain-specific results (Outcome) defined by this method in the example below and in the response status. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling.
using System;
using Hive.Axyl.Payments;
using Hive.Axyl.Core;
// payments: IPaymentsService registered during initialization (for how to retrieve it, see [Install and initialize the module](../init.md))
IPaymentsService payments = HiveCore.Resolve<IPaymentsService>();
// Right after the store payment is completed, create the request object with the receipt issued by the market
var request = new PurchaseRequest
{
AxylReceipt = receipt, // StoreKit 2 transaction JWS received from the Apple App Store
ProviderId = PurchaseRequestProviderId.Apple,
ProductId = "com.game.item.gold_100",
Price = 1200.0m,
Currency = "KRW",
Country = "KR",
Language = "ko",
Quantity = 1,
StoreTransactionId = "2000000123456789",
RequestType = 1, // 1: New purchase
RequestDate = DateTimeOffset.UtcNow,
ServerId = "server01",
AccountUuid = accountUuid
};
PaymentsRecordStorePurchaseResult result = await payments.RecordStorePurchaseAsync(request);
switch (result)
{
case PaymentsRecordStorePurchaseResult.Success success:
// Save succeeded. You can check success.Data.Meta.
break;
// Problem with request values or payment status
case PaymentsRecordStorePurchaseResult.PaymentBadRequest:
case PaymentsRecordStorePurchaseResult.PaymentInvalidParameter:
case PaymentsRecordStorePurchaseResult.PaymentResourceNotFound:
case PaymentsRecordStorePurchaseResult.PaymentUnauthorized:
case PaymentsRecordStorePurchaseResult.VerifyError:
Debug.LogError("Check the request values and the payment status.");
break;
case PaymentsRecordStorePurchaseResult.UnknownOutcome unknownOutcome:
Debug.LogWarning($"Unknown result: {unknownOutcome.Code}");
break;
// Handle common Failure
case PaymentsRecordStorePurchaseResult.Failure failure:
HiveError err = failure.Problem;
Debug.LogError($"[{err.Code}] {err.Message} (trace: {err.TraceId})");
break;
// Safety net: unhandled results
default:
Debug.LogWarning($"Unhandled result: {result.GetType().Name}");
break;
}
Response data
On success, the result is contained in Data (PurchaseResponseData) of PaymentsRecordStorePurchaseResult.Success.
| Field name | Type | Required | Description |
|---|---|---|---|
Data.Meta | string | Optional | Response meta information |
Response example
Response status
The following table summarizes the success result of PaymentsRecordStorePurchaseResult and the domain-specific results (Outcome) defined by this method. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling.
| Response case | Description | App client handling |
|---|---|---|
Success | Payment result saved | Pass the receipt to the app server and proceed to the receipt verification step |
PaymentBadRequest | The payment request cannot be processed. | Check the request values and call conditions |
PaymentInvalidParameter | The payment request parameters are invalid. | Check the request fields, such as AxylReceipt |
PaymentResourceNotFound | The payment information to save cannot be found. | Check the receipt and transaction information |
PaymentUnauthorized | No permission for the payment request. | Check the app and authentication status |
VerifyError | An error occurred during payment verification. | Check the receipt and the market payment status |
UnknownOutcome | A domain-specific result that the SDK does not recognize. | Treat it as a failure and log the result code |
Failure | Common Failure. See Common error handling. | Handle it according to the common error handling guidelines |
5. Save subscription purchase information
For subscription products, save the subscription purchase information to the Hive Axyl server with PurchaseSubscriptionAsync() before you request receipt verification from the app server. Because the Hive Axyl server uses the same receipt to find the subscription record from this step in Complete the subscription, keep the AxylReceipt you passed in this step until subscription completion.
PurchaseSubscriptionAsync
PurchaseSubscriptionAsync() saves the subscription purchase information with the subscription receipt and returns the transaction ID and lookup key of the saved subscription record.
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | SubscriptionPurchaseRequest | Required | The request data object for saving subscription purchase information. |
| context | ApiCallContext | Optional | Per-call settings object that specifies the idempotency key, cancellation token, and request policy. If omitted, the default values are used. |
SubscriptionPurchaseRequest
| Field name | Type | Required | Description |
|---|---|---|---|
AppVersion | string | Optional | The app version. |
AxylReceipt | string | Required | The StoreKit 2 transaction JWS (JwsRepresentation) of the subscription payment. |
Country | string | Required | The country code (two-letter ISO 3166-1 code). Example: KR |
Currency | string | Required | The currency (three-letter ISO 4217 code). Example: KRW |
Language | string | Required | The language code (two-letter ISO 639-1 code). Example: ko |
OriginalPrice | decimal | Optional | The pre-discount price. |
Price | decimal | Required | The subscription price. Pass the price of the product retrieved in Step 2. Get the product list. |
ProviderId | SubscriptionPurchaseRequestProviderId | Required | Because this is an Apple subscription, specify Apple. |
RequestTimeMs | long | Optional | The request time (milliseconds). |
ServerId | string | Optional | The ID of the app server registered by following Hive Console App Server. |
StoreTransactionId | string | Optional | If you pass the transaction JWS, you can omit this because the lookup key originalTransactionId is in the receipt. Even if you send it together, the value in the receipt takes precedence. |
Call example
Check the success result of PaymentsPurchaseSubscriptionResult and the domain-specific results (Outcome) defined by this method in the example below and in the response status. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling.
using Hive.Axyl.Payments;
using Hive.Axyl.Core;
// payments: IPaymentsService registered during initialization (for how to retrieve it, see [Install and initialize the module](../init.md))
IPaymentsService payments = HiveCore.Resolve<IPaymentsService>();
// Right after the subscription payment ends, create the request object with the receipt issued by the store
var request = new SubscriptionPurchaseRequest
{
AxylReceipt = transactionJws, // AppleTransaction.JwsRepresentation of the subscription payment
ProviderId = SubscriptionPurchaseRequestProviderId.Apple,
Price = 5500.0m,
Currency = "KRW",
Country = "KR",
Language = "ko",
ServerId = "server01"
};
PaymentsPurchaseSubscriptionResult result = await payments.PurchaseSubscriptionAsync(request);
switch (result)
{
case PaymentsPurchaseSubscriptionResult.Success success:
// Save succeeded. Request subscription receipt verification from the app server.
Debug.Log($"Subscription record transaction ID: {success.Data.HiveAxylTransactionId}");
break;
case PaymentsPurchaseSubscriptionResult.PaymentResourceConflict:
// The subscription is already saved. Request subscription receipt verification from the app server.
break;
// Problem with request values or payment status
case PaymentsPurchaseSubscriptionResult.PaymentBadRequest:
case PaymentsPurchaseSubscriptionResult.PaymentInvalidParameter:
case PaymentsPurchaseSubscriptionResult.PaymentResourceNotFound:
case PaymentsPurchaseSubscriptionResult.PaymentUnauthorized:
case PaymentsPurchaseSubscriptionResult.VerifyError:
Debug.LogError("Check the request values and the payment status.");
break;
case PaymentsPurchaseSubscriptionResult.UnknownOutcome unknownOutcome:
Debug.LogWarning($"Unknown result: {unknownOutcome.Code}");
break;
// Handle common Failure
case PaymentsPurchaseSubscriptionResult.Failure failure:
HiveError err = failure.Problem;
Debug.LogError($"[{err.Code}] {err.Message} (trace: {err.TraceId})");
break;
// Safety net: unhandled results
default:
Debug.LogWarning($"Unhandled result: {result.GetType().Name}");
break;
}
Response data
On success, the save result is contained in Data (SubscriptionPurchaseResponseData) of PaymentsPurchaseSubscriptionResult.Success.
| Field name | Type | Required | Description |
|---|---|---|---|
Data.HiveAxylTransactionId | string | Required | The transaction ID of the saved subscription record. |
Data.HiveAxylProductId | string? | Optional | The ID of the saved subscription product. |
Data.HiveAxylStoreTransactionId | string? | Optional | The lookup key of the subscription record. The server uses this key to find the same subscription record during subscription completion. |
Data.Meta | string? | Optional | Response meta information. |
Response status
The following table summarizes the success result of PaymentsPurchaseSubscriptionResult and the domain-specific results (Outcome) defined by this method. For the result model and handling principles of common failures (Failure) that prevent the request from being performed, see Common error handling.
| Response case | Description | App client handling |
|---|---|---|
Success | The subscription purchase information was saved. | Request subscription receipt verification from the app server |
PaymentBadRequest | The payment request cannot be processed. | Check the request values and call conditions |
PaymentInvalidParameter | The payment request parameters are invalid. | Check the request fields |
PaymentResourceConflict | The subscription purchase information is already saved. | Request subscription receipt verification from the app server |
PaymentResourceNotFound | The payment information cannot be found. | Check the receipt and transaction ID |
PaymentUnauthorized | No permission for the payment request. | Check the credentials and request permissions |
VerifyError | An error occurred during payment verification. | Check the StoreKit receipt and payment status |
UnknownOutcome | A domain-specific result that the SDK does not recognize. | Treat it as a failure and log the result code |
Failure | Common Failure. See Common error handling. | Handle it according to the common error handling guidelines |
Next steps
Proceed to Step 4. Verify receipts.