Step 5. Deliver products and finish transactions
When receipt verification succeeds, the app server delivers the product to the user, and the app client records the delivery result and then finishes the transaction. The hiveAxylTransactionId and subscription product ID that the app client uses in this step are values that the app server received in the receipt verification response and passed on. For the roles of the payment confirmation request and delivery result recording, see Confirm payments and record delivery results.
Follow the process below to deliver products and finish transactions.
1. Deliver products
After checking the receipt verification result, the app server delivers the product to the user who made the payment. You implement product delivery in the app, and the delivery method may differ by app.
Use RequestPurchaseAsync(), which sends the payment confirmation request, only for consumable products. For subscription products, confirm the subscription with 3. Complete the subscription. In Apple payments, the app decides the call order of product delivery and RequestPurchaseAsync(), but deliver the product first and then send the payment confirmation request. 2. Save product delivery results is not a prerequisite for 4. Finish the transaction, so do not delay finishing the transaction even if saving the delivery result fails.
2. Save product delivery results
ItemResultAsync
To implement saving consumable product delivery results, call ItemResultAsync() provided by the Hive Axyl SDK. It records the result on the Hive Axyl server after product delivery is complete. This method does not confirm the payment or decide whether to deliver the product.
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | ItemResultBody | Required | The request data object for saving consumable product delivery results. |
| context | ApiCallContext | Optional | Per-call settings object that specifies the idempotency key, cancellation token, and request policy. If omitted, the default values are used. |
ItemResultBody
| Field name | Type | Required | Description |
|---|---|---|---|
AxylTransactionId | string | Required | Hive Axyl payment transaction ID (the prefix identifies the market). Use the hiveAxylTransactionId value that the app server received in the receipt verification response and passed on. |
Status | int | Required | The delivery result status. 1: delivery succeeded, 2: canceled without reclaiming the product, 3: canceled with the product reclaimed; any other value is invalid. |
Assets | IReadOnlyList<ItemResultAsset> | Optional | The list of products actually delivered. If there are no items to record, omit this field. |
ProjectPayloadInfo | string | Optional | Free-form JSON data for each app (JSON string) |
ItemResultAsset
| Field name | Type | Required | Description |
|---|---|---|---|
AssetId | string | Optional | Unique ID of the product in the app |
AssetName | string | Optional | Name of the product in the app |
Quantity | int | Optional | The delivered quantity. If specified, it must be 1 or more. |
Call example
Check the success result of PaymentsItemResultResult and the domain-specific results (Outcome) defined by this method in the following example 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>();
// After product delivery is complete, create the request object with the delivery result
var request = new ItemResultBody
{
AxylTransactionId = axylTransactionId, // hiveAxylTransactionId from the receipt verification response passed by the app server
Status = 1, // 1: Delivery succeeded
Assets = new[]
{
new ItemResultAsset { AssetId = "item_gold_100", AssetName = "골드 100개", Quantity = 1 }
},
ProjectPayloadInfo = "{\"serverId\":\"server01\"}" // (Optional) Free-form JSON data for each app
};
PaymentsItemResultResult result = await payments.ItemResultAsync(request);
switch (result)
{
case PaymentsItemResultResult.Success success:
// Delivery result recorded successfully. You can check success.Data.Meta.
break;
// Problem with request values — check required values, quantity, and status value
case PaymentsItemResultResult.PaymentInvalidParameter:
case PaymentsItemResultResult.PaymentBadRequest:
Debug.LogError("Check the request parameters.");
break;
// Handle common Failure
case PaymentsItemResultResult.Failure failure:
HiveError err = failure.Problem;
Debug.LogError($"[{err.Code}] {err.Message} (trace: {err.TraceId})");
break;
// Safety net: unhandled results and unknown new results (UnknownOutcome)
default:
Debug.LogWarning($"Unhandled result: {result.GetType().Name}");
break;
}
Response data
On success, Data (SuccessResponseData) of PaymentsItemResultResult.Success contains the response meta information.
| Field name | Type | Required | Description |
|---|---|---|---|
Data.Meta | string? | Optional | The response meta information. |
Response example
Response status
The following table summarizes the success result of PaymentsItemResultResult 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 | Delivery result recorded successfully | Proceed with the next payment processing |
InvalidQuantity | The delivered quantity is invalid. | Check the delivered quantity |
InvalidStatus | The delivery result status value is invalid. | Check the Status value |
PaymentBadRequest | The request cannot be processed. | Check the request values and call conditions |
PaymentInvalidParameter | When a parameter is invalid (for example, AxylTransactionId is missing) | Check the required parameter settings |
PaymentResourceNotFound | The payment information cannot be found. | Check the receipt, order number, and payment status |
UnknownOutcome | A domain-specific result that the SDK does not know. | Treat it as a failure and record the result code |
Failure | A common Failure. See Common error handling. | Handle it according to the common error handling criteria |
3. Complete the subscription
For subscription products, after the app server finishes subscription receipt verification and delivers the subscription benefits, call PostSubscriptionAsync() to confirm the subscription on the Hive Axyl server. When the subscription is confirmed, proceed with 4. Finish the transaction.
PostSubscriptionAsync
PostSubscriptionAsync() finds the subscription record saved in Save subscription purchase information, confirms the subscription, and finishes the transaction.
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | SubscriptionPurchasePostRequest | Required | The request data object for confirming the subscription. |
| context | ApiCallContext | Optional | Per-call settings object that specifies the idempotency key, cancellation token, and request policy. If omitted, the default values are used. |
SubscriptionPurchasePostRequest
| Field name | Type | Required | Description |
|---|---|---|---|
AppVersion | string | Optional | The app version. |
AxylReceipt | string | Required | Pass, as is, the StoreKit 2 transaction JWS that you passed in the PurchaseSubscriptionAsync() request in Save subscription purchase information. The server uses this value to find the saved subscription record, so if the value differs, the subscription is not confirmed. |
Country | string | Required | The country code (ISO 3166-1, two letters). Example: KR |
Currency | string | Required | The currency (ISO 4217, three letters). Example: KRW |
Language | string | Required | The language code (ISO 639-1, two letters). Example: ko |
ProductId | string | Required | The subscription product ID. Use the hiveAxylProductId that the app server received in the subscription product receipt verification response and passed on. |
ProviderId | SubscriptionPurchasePostRequestProviderId | Required | Specify Apple because this is an Apple subscription. |
RequestTimeMs | long | Optional | The request time (milliseconds). |
RequestType | int | Optional | The request type. 1: new purchase, 2: purchase restoration |
ServerId | string | Optional | The app server ID registered by following Hive Console app server. |
Call example
Check the success result of PaymentsPostSubscriptionResult and the domain-specific results (Outcome) defined by this method in the following example 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>();
var request = new SubscriptionPurchasePostRequest
{
AxylReceipt = transactionJws, // The same JWS sent to PurchaseSubscriptionAsync()
ProductId = verifiedProductId, // hiveAxylProductId from the subscription receipt verification response passed by the app server
Country = "KR",
Currency = "KRW",
Language = "ko",
ProviderId = SubscriptionPurchasePostRequestProviderId.Apple
};
PaymentsPostSubscriptionResult result = await payments.PostSubscriptionAsync(request);
switch (result)
{
case PaymentsPostSubscriptionResult.Success success:
if (string.IsNullOrEmpty(success.Data.HiveAxylTransactionId))
{
// No subscription record to confirm was found. Check that it is the same value as the AxylReceipt sent when saving the subscription purchase information.
Debug.LogError("There is no confirmed subscription.");
break;
}
// Subscription confirmed. Proceed with finishing the transaction (FinishTransactionAsync).
break;
case PaymentsPostSubscriptionResult.VerifyDuplicated:
// The subscription is already confirmed. Proceed with finishing the transaction (FinishTransactionAsync).
break;
// Problem with request values or payment status
case PaymentsPostSubscriptionResult.PaymentBadRequest:
case PaymentsPostSubscriptionResult.PaymentInvalidParameter:
case PaymentsPostSubscriptionResult.PaymentResourceNotFound:
case PaymentsPostSubscriptionResult.PaymentUnauthorized:
case PaymentsPostSubscriptionResult.VerifyError:
Debug.LogError("Check the request values and the payment status.");
break;
case PaymentsPostSubscriptionResult.UnknownOutcome unknownOutcome:
Debug.LogWarning($"Unknown result: {unknownOutcome.Code}");
break;
// Handle common Failure
case PaymentsPostSubscriptionResult.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, Data (SubscriptionPurchasePostResponseData) of PaymentsPostSubscriptionResult.Success contains the confirmation result. Even if Success is returned, Data.HiveAxylTransactionId is empty when no subscription record to confirm is found, so use this value to check whether the subscription was actually confirmed.
| Field name | Type | Required | Description |
|---|---|---|---|
Data.HiveAxylProductId | string | Required | The subscription product ID of the confirmation request. |
Data.HiveAxylStoreTransactionId | string? | Optional | The lookup key of the subscription record. |
Data.HiveAxylTransactionId | string? | Optional | The transaction ID of the confirmed subscription record. Empty if there is no subscription record to confirm. |
Data.Meta | string? | Optional | The response meta information. |
Response status
The following table summarizes the success result of PaymentsPostSubscriptionResult 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 request was processed. Check whether the subscription was actually confirmed with Data.HiveAxylTransactionId. | If the value exists, proceed with finishing the transaction; if it is empty, check AxylReceipt and whether the subscription purchase information was saved |
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 |
PaymentResourceNotFound | The payment information cannot be found. | Check the receipt and transaction information |
PaymentUnauthorized | There is no permission for the payment request. | Check the credentials and request permission |
VerifyDuplicated | The subscription is already confirmed. | Proceed with finishing the transaction without confirming it again |
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 know. | Treat it as a failure and record the result code |
Failure | A common Failure. See Common error handling. | Handle it according to the common error handling criteria |
4. Finish the transaction
Call FinishTransactionAsync() of the Apple payment plugin (IAppleStoreKitPlugin) to notify the Apple App Store that the transaction is complete. For consumable products, call it after you complete product delivery and the payment confirmation request; for subscription products, call it after you complete the subscription. For consumable products, the user can purchase the same product again after the transaction is finished.
FinishTransactionAsync
Call parameters
| Field name | Type | Required | Description |
|---|---|---|---|
| request | FinishTransactionRequest | Required | The request data object for finishing the transaction. |
| ct | CancellationToken | Optional | The cancellation token. |
FinishTransactionRequest
| Field name | Type | Required | Description |
|---|---|---|---|
TransactionId | uint64 | Required | The Apple transaction ID to finish. Use the AppleTransaction.Id value from the PurchaseAsync() success response. |
Call example
Check the success result of AppleStoreKitServiceFinishTransactionResult and the domain-specific results (Outcome) defined by this method in the following example 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.Core;
using Hive.Axyl.Payments.Addon.Apple;
using System.Threading;
// Retrieve the Apple StoreKit plugin
if (!HiveCore.TryResolve<IAppleStoreKitPlugin>(out var applePlugin))
{
Debug.LogError("The Apple StoreKit plugin is not registered.");
return;
}
// Finish the transaction with the transaction ID received in the PurchaseAsync() success response
var request = new FinishTransactionRequest
{
TransactionId = transactionId // AppleTransaction.Id value
};
AppleStoreKitServiceFinishTransactionResult result =
await applePlugin.FinishTransactionAsync(request, CancellationToken.None);
switch (result)
{
case AppleStoreKitServiceFinishTransactionResult.Success:
// Transaction finished successfully. The user can purchase the same product again.
Debug.Log("Transaction finished successfully");
break;
case AppleStoreKitServiceFinishTransactionResult.TransactionNotFound:
// When the transaction ID cannot be found
Debug.LogError("Transaction not found. Check the transaction ID.");
break;
case AppleStoreKitServiceFinishTransactionResult.UnknownOutcome:
Debug.LogWarning("Unknown result.");
break;
case AppleStoreKitServiceFinishTransactionResult.Failure failure:
Debug.LogError($"Failed to finish the transaction: {failure}");
break;
default:
Debug.LogWarning($"Unhandled result: {result.GetType().Name}");
break;
}
Response data
No data is returned on success.
Response status
The returned AppleStoreKitServiceFinishTransactionResult object branches into one of the following cases. Handle them with a switch statement.
| Response case | Description | App client handling |
|---|---|---|
Success | Transaction finished successfully | End the payment flow |
TransactionNotFound | When the transaction ID cannot be found | Check the TransactionId value |
UnknownOutcome | Unknown result | Retry or show an error message |
Failure | A common Failure. See Common error handling. | Handle it according to the common error handling criteria |
Learn more
If a product could not be delivered during payment because of a network error or a similar problem, implement Restore purchases.