Implement Steam payments
To sell consumable products on Steam and deliver them to users, complete the following procedure in order.
Before you begin, complete the common prerequisites.
Overall flow
Steam payments follow a different order from other markets. The Hive Axyl server first creates the order and issues the receipt, and the user approves the payment afterward in the Steam overlay. The app must receive the approval result directly through a callback. In step 11, the app server handles receipt verification and product delivery with the Hive Axyl Server API.
| Step | Category | What you do |
|---|---|---|
| 1 | Store console | Issue a Publisher Web API Key and register products in Steamworks |
| 2 | Hive Console | Register credentials, set price tiers, and register products |
| 3 | Hive Axyl SDK, Store plugin | Install and register the SDK modules and the Steam payment plugin |
| 4 | Recipe code | Copy the recipe folders |
| 5 | App code, Hive Axyl SDK | Get the SteamID and the product list |
| 6 | Recipe code | Create a PurchaseOrder |
| 7 | Store plugin | Start receiving payment authorization callbacks |
| 8 | Recipe code | Start the purchase |
| 9 | Store plugin | Receive the user's approval result |
| 10 | Recipe code | Prepare purchase information |
| 11 | App code, App server, Hive Axyl Server API | Verify receipts and deliver products |
| 12 | Recipe code | Finish the transaction |
| 13 | Hive Axyl SDK, Store plugin | Record delivery results and stop receiving callbacks |
| 14 | Hive Console, Hive Axyl Server API | Check the purchase history |
Finish the transaction only after product delivery is complete
Finishing the transaction in step 12 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 Steamworks
-
Store console Configure this in Steamworks.
Detailed procedure: Steam console payment integration
Issue a Publisher Web API Key in Steamworks and register the products to sell. The Hive Axyl server uses this key to request order creation and payment confirmation from Steam.
Use the product IDs registered in Steamworks as is in step 2.
2. Configure the Hive Console
- Hive Console Configure or check this in the Hive Console.
Register the key you got in step 1 in the Hive Console, and register price tiers and products.
| Setting | Required | Where to check |
|---|---|---|
| Register the Publisher Web API Key | Required | Steam credentials |
| Set price tiers | Required | Register a price tier |
| Register products | Required | Register product information |
Unlike Google Play and the Apple App Store, Steam payments charge the price registered in the Hive Console. If you skip setting price tiers, the payment amount is not determined.
Match the product IDs in both consoles
The product ID registered in Steam and the product ID registered in the Hive Console must be the same. If the values differ, receipt verification fails.
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 Steam 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.steam: Receiving payment authorization callbacks
Initialize Steamworks first
When the app starts, you must call SteamAPI.Init() and run SteamAPI.RunCallbacks() every frame. If you use the plugin while Steamworks is not initialized, a FAILED_PRECONDITION error occurs.
The following example code registers the authentication, token, and payment modules and the Steam 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.Steam;
var config = CoreConfig.CreateBuilder("{appId}").Build();
HiveBootstrap.Initialize(config, builder =>
{
builder.AddAuth();
builder.AddToken();
builder.AddPayments(sandbox: true);
builder.AddSteamMicrotransactions();
});
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.Steam/: Code for Steam
To call recipes from your app code, add Hive.Axyl.Samples.Recipes, Hive.Axyl.Samples.Recipes.Payments, and Hive.Axyl.Samples.Recipes.Payments.Steam to references in your app's assembly definition.
It compiles even without the Steam payment plugin
Payments.Steam/ does not reference the Steam payment plugin, so it always compiles. The app receives payment authorization callbacks directly, outside the recipe.
5. Get the SteamID and the product list
-
App code Implement this in the app.
Hive Axyl SDK Call the Hive Axyl SDK from the app.
There is no recipe for getting the product list. For Steam, the Hive Axyl server determines even the country and currency based on the user's SteamID, so make the following calls directly, in order.
| Step | Category | Call | Where to check |
|---|---|---|---|
| 1 | App code | Get the user's 64-bit SteamID from Steamworks | Get the Steam user ID |
| 2 | Hive Axyl SDK | Get the product list with FetchSteamProductsAsync() | Get product details |
Put ProductId, Price, and Currency from the result and the SteamID from step 1 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: steamId, // 64-bit SteamID obtained in step 5
serverId: "server01",
iapPayload: "{\"character\":\"A\"}",
appVersion: Application.version,
accountUuid: accountUuid);
storePlayerId is required for Steam payments. The Hive Axyl server uses this value to create the order in Steam.
AccountUuid.TryCompute() is a recipe helper that creates a UUIDv5 from the logged-in user's playerId. Steam 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.
7. Start receiving payment authorization callbacks
-
Store plugin Call the store payment plugin from the app.
Detailed procedure: Receive the Steam payment authorization callback
Subscribe to the Steam payment authorization callback before you start the purchase. The approval result can arrive right after you call step 8.
The following example code first subscribes to the MicroTxnAuthorizationResponse event and then starts receiving callbacks. To avoid missing approval results that arrive before step 8 finishes, it collects the approval status Authorized by order number OrderId. If receiving callbacks cannot start, you cannot receive the approval result, so do not start the purchase. In the usage example, IPurchaseApplication.ObservePayment() plays the same role.
using System;
using System.Collections.Concurrent;
using Hive.Axyl.Core;
using Hive.Axyl.Payments.Addon.Steam;
var steamPlugin = HiveCore.Resolve<ISteamMicrotransactionsPlugin>();
// Collect the approval status by order number.
var approvals = new ConcurrentDictionary<ulong, bool>();
Action<SteamMicroTxnResponse> onResponse =
response => approvals[response.OrderId] = response.Authorized;
steamPlugin.MicroTxnAuthorizationResponse += onResponse;
var startResult = await steamPlugin.StartCallbackListenerAsync(CancellationToken.None);
switch (startResult)
{
case SteamMicrotransactionsServiceStartCallbackListenerResult.Success:
case SteamMicrotransactionsServiceStartCallbackListenerResult.AlreadyStarted:
// Proceed to step 8.
break;
default:
// Receiving callbacks could not start. Do not start the purchase.
return;
}
8. Start the purchase
- Recipe code Call the recipe code from the app.
When the user selects the payment button, call the recipe's InitiatePurchaseAsync(). The Hive Axyl server creates the order in Steam and returns the receipt.
started.Pending contains the receipt and the order information. 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 receipt and order information of this purchase even if verification or finishing fails.
pendingStore in the example code is a store that the app creates, and it must remain even when the app is relaunched. Save it with the account and OrderId as the key, and delete it only after the transaction is finished successfully. In the usage example, SavePendingAsync() and MarkClosedAsync() of IPurchaseApplication play the same role. Do not write receipt and token values to logs.
var recipe = new ConsumablePurchaseRecipe(new SteamPurchaseSource());
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.Success)
{
Debug.LogError($"{started.FailedStep}: {started.BusinessOutcome} {started.Error?.Message}");
return;
}
// The user has not approved it yet. Check the approval result in step 9.
The recipe performs the following tasks internally.
| Category | Call | Where to check |
|---|---|---|
| Hive Axyl SDK | Create the Steam order with InitiatePurchaseAsync() | Initialize the Steam payment session |
Receiving the receipt does not mean the payment is complete
Steam issues the receipt before the user approves the payment. Unlike other markets, the user has not been charged yet at this point. Do not deliver the product before you receive the approval result in step 9.
Steam payments have no step for saving pre-purchase information, because the order creation call also handles that record.
9. Receive the user's approval result
-
Store plugin Check the approval result of the store payment plugin in the app.
Detailed procedure: Receive the payment authorization status through the MicroTxnAuthorizationResponse event
When the user approves or cancels the payment in the Steam overlay, the MicroTxnAuthorizationResponse event delivers the order number and the approval status. Proceed to the next step only after you confirm that this order was approved, and if it was not approved, do not call step 10.
Among the results collected in step 7, only the result whose OrderId equals started.Pending.OrderId is the result of this order. The event's OrderId is a ulong and started.Pending.OrderId is a string, so convert them to the same format before comparing. The approval result may already have arrived before step 8 finished, so check the collected results first and wait for its arrival only if it is not there. The recipe does not wait for this event, so the app decides the wait time and the screen handling. In the usage example, IPurchaseApplication.WaitUntilVerifiableAsync() waits for the approval result of this order. An order that was not approved is not charged, so delete the purchase information saved in step 8.
10. Prepare purchase information
- Recipe code Call the recipe code from the app.
After you confirm the approval, call PreparePurchaseAsync() to record the purchase result on the Hive Axyl server and prepare the purchase information to send to the app server. When the Hive Axyl server receives this record request, it checks the user's approval with Steam. 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;
}
// Send prepared.Pending to the app server in step 11.
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 and confirm authorization |
11. 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 10 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.
11.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 13
// 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 13.
11.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:STEAMaxylReceipt: The receipt issued by the Hive Axyl server, contained inVerifyTokenproductId,price,currency,country,language,serverId,iapPayload,accountUuid,appVersion: The values of the same names inOrderorderId:OrderIdstoreTransactionId:StoreTransactionIdrequestType:1, which indicates a new purchase
Steam payments are charged at receipt verification
When the Hive Axyl server receives a receipt verification request from the app server, it checks the order with Steam and then proceeds with the actual charge. Do not repeat verification requests with the same receipt unnecessarily. If you do not receive a response and cannot tell whether the request was processed, send the same request again and check whether it was already processed with the hiveAxylDuplicated value in the response.
11.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 12. Also return
hiveAxylTransactionIdfrom the verification response so that it can be used to record the delivery result in step 13.
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.
12. 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 11, the recipe does not finish the transaction and returns Failure.
ClosePurchaseOutcome closed = await recipe.ClosePurchaseAsync(
prepared.Pending, // Value returned by step 10
delivery, // Value created in step 11 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 Steam payments, the transaction is finished on the Hive Axyl server, not on the device, so the store plugin is not called. 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 |
13. Record delivery results
-
Hive Axyl SDK Call the Hive Axyl SDK from the app.
Store plugin Call the store payment plugin 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 11.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 11.1 and kept separately
Status = 1, // 1: Delivery succeeded
Assets = new[]
{
new ItemResultAsset { AssetId = "gold", AssetName = "골드", Quantity = 100 },
},
});
When you close the payment screen and no longer need authorization callbacks, remove the onResponse registered in step 7 from MicroTxnAuthorizationResponse with -=, and also stop receiving callbacks in the store plugin. For how to do this, see Stop receiving Steam callbacks.
14. 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 SDK reported the payment as canceled. | 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
PreparePurchaseOutcome and ClosePurchaseOutcome return the Pending they received as is, even when they fail. Replace the purchase information saved in step 8 with the returned Pending so that you can finish that purchase later. Even when the app server could not confirm delivery in step 11, 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 Steam purchases.