Skip to main content

Payments

note

We will soon be deprecating credits for product purchases. Prices will be specified by price and currency properties.

The Jest platform allows developers to sell in-game products using the Payment SDK methods. We enable these transactions primarily through popular digital wallets, such as Apple Pay and Google Wallet. Jest handles the entire checkout process with the player, while your game is responsible for granting the purchased items and confirming the purchase.

Payments on Jest
Payments on Jest via digital wallets

Pricing

Players purchase in-game products in USD on the Jest platform.

As a developer, you define the products and USD prices available in your game. When your game calls BeginPurchase, Jest handles checkout end-to-end with the player.

note

When using a sandbox user, players see real product prices in the game UI, but the platform checkout modal makes clear that no charge will be made. The resulting PurchaseData records 0 in both price and credits (deprecated).

Purchase lifecycle

A purchase represents a single successful checkout of a product by a specific player.

  1. Display available products to the player (via GetProducts()).
  2. Start checkout by calling BeginPurchase(sku).
  3. If checkout succeeds, the SDK returns an incomplete purchase containing a purchaseToken.
  4. Grant the purchased item in your game, then confirm the purchase by calling CompletePurchase(purchaseToken).
  5. Until completed, the purchase remains incomplete and will continue to be returned by GetIncompletePurchases() (for example, after a crash).

Checkout cancellation and error handling are managed by your game; see Start a purchase (BeginPurchase) for details.

Happy path (checkout succeeds)

Recovery (startup reconciliation)

If checkout succeeds but the game crashes, loses connectivity, or is closed before CompletePurchase is called, the purchase remains incomplete. The platform will keep returning it via GetIncompletePurchases() until you confirm it.

If hasMore is true, continue fetching until all incomplete purchases are processed.

How to use the SDK

Payload shapes

Where the SDK references PurchaseData, it contains:

public class PurchaseData
{
public string purchaseToken; // Required to call CompletePurchase
public string productSku; // The product purchased
public decimal credits; // (Deprecated, use price and currency instead) Total USD value
public long createdAt; // Unix timestamp (ms since epoch)
public long? completedAt; // Unix timestamp; null until confirmed
public decimal estimatedRevenue; // The (estimated) USD share of revenue from this purchase that the publisher will receive
public decimal price; // The purchase price in the currency specified in `currency`
public string currency; // The currency (ISO 4217 code) the price is in
}

Set up products

Set up and price products using the Jest Developer Console.

For more information, see Manage products.

List products (GetProducts)

To retrieve the products available for purchase in your game, call GetProducts.

var payment = JestSDK.Instance.Payment;
var productsTask = payment.GetProducts();

await productsTask;
if (productsTask.IsCompleted)
{
List<Payment.Product> products = productsTask.Result;
foreach (var product in products)
{
Debug.Log($"Product: {product.name} ({product.sku}) - {product.price} {product.currency}");
}
}

Products include the following information:

PropertyNote
skuAn identifier for the product, configured when set up in the Developer Console.
nameThe display name of the product that may be shown to the player during checkout.
descriptionNullable short description of the product that may be shown to the player during checkout.
priceProduct price in the currency specified in currency
currencyThe currency code (ISO 4217) the price is in

Displaying prices

The way product prices are displayed should be based on the price and the currency. currency is an ISO 4217 which can be used to select the correct currency symbol or formatting.

Start a purchase (BeginPurchase)

Call BeginPurchase with the sku of a product returned by GetProducts().

The method returns a PurchaseResult with one of the following outcomes:

Success

  • result == "success" with purchase and purchaseSigned populated. Checkout completed successfully. The returned purchase is incomplete and must be granted and confirmed by your game.

Cancellation

  • result == "cancel" The player canceled the checkout flow.

Error

  • result == "error" with error containing one of:
    • internal_error - A transient error occurred. Your game may retry the purchase.
    • invalid_product - The requested sku is not available for purchase. Do not retry with the same sku.
var payment = JestSDK.Instance.Payment;
var purchaseTask = payment.BeginPurchase("gems_100");

await purchaseTask;

if (purchaseTask.Result.result == "cancel")
{
// Handle cancellation with UI feedback
return;
}

if (purchaseTask.Result.result == "error")
{
Debug.LogError($"Purchase failed: {purchaseTask.Result.error}");
// Handle the error with different product or a retry
return;
}

// result == "success"
var purchase = purchaseTask.Result.purchase;
var purchaseSigned = purchaseTask.Result.purchaseSigned;
Debug.Log($"Purchase successful: {purchase.productSku}");

Grant and confirm a purchase (CompletePurchase)

When BeginPurchase returns success, your game must:

  1. Grant the purchased product to the player.
  2. Confirm the purchase by calling CompletePurchase(purchaseToken).

Until CompletePurchase is called, the purchase remains incomplete and will continue to be returned by GetIncompletePurchases().

warning

Only call CompletePurchase after you have durably granted the purchase (for example, your backend has verified the signed data and recorded the grant). If you confirm first and then crash before granting, GetIncompletePurchases() cannot recover the purchase because it's already confirmed.

// Recommended: send purchaseSigned to your backend to verify and grant
await VerifyAndGrantPurchaseServerSide(purchaseSigned);

// Only confirm after the grant succeeded
var completeTask = JestSDK.Instance.Payment.CompletePurchase(purchase.purchaseToken);
await completeTask;

if (completeTask.Result.result == "error")
{
if (completeTask.Result.error == "internal_error")
{
// Retry later. Leaving the purchase incomplete is safe.
}
else
{
// invalid_token: don't retry with the same token.
}
}

CompletePurchase returns:

  • Success (result == "success").
  • Error (result == "error") with error being:
    • internal_error: a transient error occurred; retry.
    • invalid_token: the token is not valid (already confirmed, wrong player, etc.); don't retry with the same token.
tip

If you have a backend, treat purchase.purchaseToken as an idempotency key and store it so you never grant the same purchase twice, including during retries or recovery flows.

Recover incomplete purchases (GetIncompletePurchases)

Your game must check for incomplete purchases every time it starts. This is what makes purchases resilient to crashes, power loss, and network failures between a successful checkout and CompletePurchase.

var payment = JestSDK.Instance.Payment;
IncompletePurchasesResponse result;

do
{
var incompletePurchasesTask = payment.GetIncompletePurchases();
await incompletePurchasesTask;
result = incompletePurchasesTask.Result;

// Recommended: send purchasesSigned to your backend to verify and grant
await VerifyAndGrantPurchasesServerSide(result.purchasesSigned);

foreach (var purchase in result.purchases)
{
var completeTask = payment.CompletePurchase(purchase.purchaseToken);
await completeTask;

if (completeTask.Result.result == "error")
{
if (completeTask.Result.error == "internal_error")
{
// Retry later. Leaving it incomplete is safe.
return;
}
// invalid_token: don't retry with the same token.
}
}
} while (result.hasMore);

The response is capped (currently 50 purchases per call). If hasMore is true, confirm the returned purchases and call again until it is false.

Signed purchase data (JWT)

The BeginPurchase and GetIncompletePurchases methods return purchase data in two forms:

  1. As plain objects (purchase / purchases) for convenience.
  2. As signed tokens (purchaseSigned / purchasesSigned) in the form of a signed JSON Web Token (JWT).

The data inside the signed token is equivalent to the plain object. For critical actions such as granting items or crediting accounts, you must only trust the signed token after verifying its signature.

For server-side verification examples and details, see the HTML5 SDK Payments documentation.

warning

Failure to verify the signed token can leave your game vulnerable to exploitation.

Revenue estimate

Jest's economics model splits revenue between the publisher, the platform and publishers who bring users in to the platform. PurchaseData includes an estimate, in USD, of the share you (as the publisher) are entitled to after the purchase is completed.

At the time of purchase, this is an estimate only, as the final revenue may depend on player subscription or other factors. See our economics model for more information.

Final revenue will be reported via the developer console (details coming soon).