Skip to content

API

After Balancy is initialized, a wide range of functions and methods are available to interact with the Balancy SDK.

Purchasing

Balancy provides robust purchasing methods that integrate seamlessly with the inventory system. After a successful purchase:

  • Items are automatically added to the inventory.
  • If the price is in soft currency, the amount is deducted from the inventory.
  • If the price is in watching Ads, the amount of watched Ads for this item is reset.
  • If there are insufficient items in the inventory or not enough Ads watched, the purchase fails and returns FALSE.

For Unity there are 2 ways to work with payments, we recommend using the High Level one. For Type Script you can only use Low Level for now only.

High Level

You can use the following methods to make purchases:

Balancy.API.InitPurchase(storeItem, (success, error) => Debug.Log("Purchase complete: " + success + " error = " + error));
Balancy.API.InitPurchaseOffer(offerInfo, (success, error) => Debug.Log("Purchase complete: " + success + " error = " + error));
Balancy.API.InitPurchaseOffer(offerGroupInfo, storeItem, (success, error) => Debug.Log("Purchase complete: " + success + " error = " + error));
Balancy.API.initPurchase(storeItem, (success, error) => console.log("Purchase complete: " + success + " error = " + error));
Balancy.API.initPurchaseOffer(offerInfo, (success, error) => console.log("Purchase complete: " + success + " error = " + error));
Balancy.API.initPurchaseOfferGroup(offerGroupInfo, storeItem, (success, error) => console.log("Purchase complete: " + success + " error = " + error));

Using Balancy Package (Unity Only)

  1. Make sure you have installed the Unity's In App Purchasing package version 4.12+.
  2. Install the latest Balancy Payments package: Balancy Purchasing.
  3. Add Balancy.Payments asmdef into your project assembly definition.
  4. To avoid conflicts, make sure you are not invoking UnityPurchasing.Initialize in your code.

How it works under the hood:

  1. When Balancy.Callbacks.OnDataUpdated is invoked, Balancy automatically grabs all Products you defined in the Balancy dashboard and calls UnityPurchasing.Initialize with them.
  2. After you Init the purchase using one of the methods above, Balancy automatically defined the Price type, does the required checks and invokes all the required methods.
  3. The purchase result is returned via the callback you provided.
  4. Balancy also automatically caches the purchase receipt, in case the app crashes or looses connection during the purchase process. The receipt is sent to the server when the app reconnects. For this case, please subscribe to the following callbacks to be notified:

    Balancy.Callbacks.OnHardPurchasedStoreItem += (paymentInfo, storeItem) => Debug.Log(" => Balancy.OnHardPurchasedStoreItem: " + storeItem?.Name + " UnnyId = " + storeItem?.UnnyId);
    Balancy.Callbacks.OnHardPurchasedOffer += (paymentInfo, gameOffer) => Debug.Log(" => Balancy.OnHardPurchasedOffer: " + gameOffer?.Name + " UnnyId = " + gameOffer?.UnnyId);
    Balancy.Callbacks.OnHardPurchasedOfferGroup += (paymentInfo, gameOfferGroup, storeItem) => Debug.Log(" => Balancy.OnHardPurchasedOfferGroup: " + gameOfferGroup?.Name + " UnnyId = " + gameOfferGroup?.UnnyId);
    

Using custom Purchasing

In the case you are using your own implementation of Purchasing, you have to connect with Balancy SDK:

Let Balancy know, which method to use for hard purchases:

Balancy.Actions.Purchasing.SetHardPurchaseCallback(TryToHardPurchase);
Balancy.Actions.Purchasing.setHardPurchaseCallback(tryToHardPurchase);

Balancy SDK will invoke the TryToHardPurchase method inside of the Balancy.API.Init<> method if case the price is in hard currency.

Once the purchase is completed, you have to call the following method:

Balancy.API.FinalizedHardPurchase(Balancy.Actions.PurchaseResult.Success, productInfo, paymentInfo, (validationSuccess, removeFromPending) =>
{
    Debug.Log("Validation result = " + validationSuccess);
});
Balancy.API.finalizedHardPurchase(true, productInfo, paymentInfo);

Make sure to define all the parameters of paymentInfo object, otherwise the purchase will be marked as failed.

public class PaymentInfo
{
   public float Price;
   public string Receipt;
   public string ProductId;
   public string Currency;
   public string OrderId;
}
export class BalancyPaymentInfo {
constructor(
   public price: number,
   public receipt: string,
   public productId: string,
   public currency: string,
   public orderId: string) {}
}

If something goes wrong, you can call the following method to notify Balancy about the error:

Balancy.API.FinalizedHardPurchase(Actions.PurchaseResult.Failed, productInfo, new Actions.PurchaseInfo
{
    ErrorMessage = "Product ID is null or empty"
}, null);
Balancy.API.finalizedHardPurchase(false, productInfo, null);

In both methods productInfo is the product you are trying to purchase. It was sent to you via the SetHardPurchaseCallback callback.

Low Level

Soft Purchasing

Soft purchasing methods return a bool value indicating whether the purchase was successful.

Balancy.API.SoftPurchaseShopSlot(shopSlot);
Balancy.API.SoftPurchaseGameOffer(offerInfo);
Balancy.API.SoftPurchaseGameOfferGroup(offerGroupInfo, storeItem);
Balancy.API.softPurchaseShopSlot(shopSlot);
Balancy.API.softPurchaseGameOffer(offerInfo);
Balancy.API.softPurchaseGameOfferGroup(offerGroupInfo, storeItem);

There is an additional method you can use to directly purchase StoreItem, but it's better to avoid using it. Use it only if you sell something outside of Balancy features.

Balancy.API.SoftPurchaseStoreItem(storeItem);
Balancy.API.softPurchaseStoreItem(storeItem);

Hard Purchasing

Hard purchasing methods require a PaymentInfo object containing payment details. This object is used for validation and segmentation purposes. The purchase result is returned via a callback.

var paymentInfo = new Balancy.Core.PaymentInfo
{
    Price = (float)unityProduct.metadata.localizedPrice,
    Receipt = unityProduct.receipt,
    ProductId = unityProduct.definition.id,
    Currency = unityProduct.metadata.isoCurrencyCode,
    OrderId = unityProduct.transactionID
};

void PurchaseCompleted(Balancy.Core.Responses.PurchaseProductResponseData responseData) {
    Debug.Log("Success = " + responseData.Success);
    Debug.Log("ErrorCode = " + responseData.ErrorCode);
    Debug.Log("ErrorMessage = " + responseData.ErrorMessage);
    Debug.Log("ProductId = " + responseData.ProductId);
}

Balancy.API.HardPurchaseShopSlot(shopSlot, paymentInfo, PurchaseCompleted, requireValidation);
Balancy.API.HardPurchaseGameOffer(offerInfo, paymentInfo, PurchaseCompleted, requireValidation);
Balancy.API.HardPurchaseGameOfferGroup(offerGroupInfo, storeItem, paymentInfo, PurchaseCompleted, requireValidation);
import {Balancy, BalancyPaymentInfo} from '@balancy/core';    
import {BalancyPurchaseProductResponseData} from "@balancy/wasm";

function purchaseCompleted(response: BalancyPurchaseProductResponseData): void {
    console.log("Response received:");
    console.log("Success:", response.success);
    console.log("Error Code:", response.errorCode);
    console.log("Error Message:", response.errorMessage);
    console.log("ProductId: ", response.productId);
}

let paymentInfo = new BalancyPaymentInfo(...);

Balancy.API.hardPurchaseShopSlot(shopSlot, paymentInfo, purchaseCompleted, requireValidation);
Balancy.API.hardPurchaseGameOffer(offerInfo, paymentInfo, purchaseCompleted, requireValidation);
Balancy.API.hardPurchaseGameOfferGroup(offerGroupInfo, storeItem, paymentInfo, purchaseCompleted, requireValidation);

There is an additional method you can use to directly purchase StoreItem, but it's better to avoid using it. Use it only if you sell something outside of Balancy features.

Balancy.API.HardPurchaseStoreItem(storeItem, paymentInfo, PurchaseCompleted, requireValidation);
Balancy.API.hardPurchaseStoreItem(storeItem, paymentInfo, purchaseCompleted, requireValidation);

Products Retrieval

Balancy provides methods to retrieve product information from configured platforms (e.g., Google Play, App Store).

Get All Products

Retrieves all available products asynchronously.

Balancy.API.GetProducts(response =>
{
    if (response.Success)
    {
        foreach (var product in response.Products)
        {
            Debug.Log($"Product: {product.Name} => {product.ProductId}");
            Debug.Log($"  Platform Product ID: {product.PlatformProductId}");
            Debug.Log($"  Price: {product.Price}");
            Debug.Log($"  Type: {product.Type}");
        }
    }
    else
    {
        Debug.LogError($"Failed to get products: {response.ErrorMessage} (Code: {response.ErrorCode})");
    }
});
Balancy.API.getProducts(response => {
    if (response.success) {
        response.products.forEach(product => {
            console.log(`Product: ${product.name} => ${product.productId}`);
            console.log(`  Platform Product ID: ${product.platformProductId}`);
            console.log(`  Price: ${product.price}`);
            console.log(`  Type: ${product.type}`);
        });
    } else {
        console.error(`Failed to get products: ${response.errorMessage} (Code: ${response.errorCode})`);
    }
});

Get Single Product

Retrieves a specific product by ID.

Balancy.API.GetProduct("gold.pack.1", response =>
{
    if (response.Success && response.Product != null)
    {
        Debug.Log($"Product: {response.Product.Name} => {response.Product.ProductId}");
    }
    else
    {
        Debug.LogError($"Product not found: {response.ErrorMessage}");
    }
});
Balancy.API.getProduct("gold.pack.1", response => {
    if (response.success && response.product !== null) {
        console.log(`Product: ${response.product.name} => ${response.product.productId}`);
    } else {
        console.error(`Product not found: ${response.errorMessage}`);
    }
});

Product Response Structure

The response contains:

  • Success/success: Whether the operation succeeded
  • ErrorCode/errorCode: Error code if operation failed
  • ErrorMessage/errorMessage: Error message if operation failed
  • Product/product: The product object (null if not found)
  • Products/products: Array of products (for GetProducts)

Balancy Status

The GetStatus method returns the current status of the Balancy SDK.

var status = Balancy.API.GetStatus();
const status = Balancy.API.getStatus();

Don't cache the status object because it can change after each call.

Parameter Description
BranchName The name of the branch the SDK is connected to.
Deploy The unique number of the Deploy. You can the version of configs, that's being used
LastCloudConfigsUpdate The unix time (seconds) of the last successful synchronization of the game configs during this session.
ServerTime The server time in seconds since 1970.
GameTime The game time in seconds since 1970. The Game time is used for all the conditions and can be changed in the Balancy Dashboard.
ServerTimeMs The server time in milliseconds.
GameTimeMs The game time in milliseconds.

Custom Time Provider

The Balancy SDK maintains an internal clock synchronized with the server. All time-dependent features — offers, events, battle passes, daily bonuses, A/B tests, cooldowns, timers, conditions — use this internal clock.

The Custom UTC Time Provider lets you replace this internal clock with your own time source. When set, the SDK calls your function on every time query instead of computing time internally.

When to use

  • Your game has its own authoritative time server (NTP, Firebase, custom backend) and you want the SDK to use it.
  • You need to simulate a specific date/time in a controlled testing environment.
  • You need a production-safe alternative to SetTimeCheatingOffset (which only works in development builds).

Set Custom Time Provider

// Use your own NTP time
Balancy.API.SetCustomUTCTimeProvider(() => MyNtpClient.GetCurrentUtcTime());
// Use your own time server (return UTC seconds since Unix epoch)
API.setCustomUTCTimeProvider(() => myTimeServer.getUtcSeconds());

The provider must return UTC time:

  • C#: Return a DateTime with DateTimeKind.Utc. If you pass a local time, it will be converted via ToUniversalTime(), but returning UTC directly is recommended.
  • TypeScript: Return seconds since Unix epoch (same as Math.floor(Date.now() / 1000)).

Examples

Simulate a specific date:

Balancy.API.SetCustomUTCTimeProvider(() =>
    new DateTime(2025, 12, 25, 10, 0, 0, DateTimeKind.Utc)
);
const christmas2025 = Math.floor(new Date('2025-12-25T10:00:00Z').getTime() / 1000);
API.setCustomUTCTimeProvider(() => christmas2025);

Offset from real time (production-safe):

var offset = TimeSpan.FromHours(3);
Balancy.API.SetCustomUTCTimeProvider(() => DateTime.UtcNow + offset);
const offsetSeconds = 3 * 3600; // 3 hours ahead
API.setCustomUTCTimeProvider(() => Math.floor(Date.now() / 1000) + offsetSeconds);

Reset Custom Time Provider

Removes the custom provider and returns to the default server-synced clock.

Balancy.API.ResetCustomUTCTimeProvider();
API.resetCustomUTCTimeProvider();

Important notes

  • The provider is called synchronously on every time query (every update tick, timer check, condition evaluation). Your callback must be fast — avoid network calls or heavy computation inside it. Cache the result and update it periodically if your time source is expensive.
  • The custom provider survives SDK re-initialization (Init / Stop + Init). It is only cleared when you explicitly call ResetCustomUTCTimeProvider.
  • You can set the provider before calling Init() — it will be used immediately.
  • SetTimeCheatingOffset offset is still applied on top of the custom provider's value, so QA testers can shift time relative to whatever the provider returns.
  • All SDK time-dependent features respect the custom provider: offers, events, battle passes, daily bonuses, A/B tests, conditions, session tracking, analytics timestamps, and cloud sync timestamps.

Difference from SetTimeCheatingOffset

SetTimeCheatingOffset SetCustomUTCTimeProvider
How it works Adds a fixed offset (in seconds) to the internal clock Replaces the internal clock entirely with your callback
Works in production No (silently ignored) Yes
Precision Relative — shift time by N seconds Absolute — you control the exact time
Use case Dev/QA testing Production + testing

Ads

You can use Balancy SDK to track Ad revenue, this data can later be used for segmentation.

Balancy.API.TrackAdRevenue(<AdType>, <revenue>, <placement>);
Balancy.API.trackAdRevenue(<AdType>, <revenue>, <placement>);

When using methods, like Balancy.API.InitPurchase, Balancy might initiate the ad watching process. You can set a callback to be notified when an ad should start - you still have to show the Ad and then report back to Balancy that the Ad was watched using storeItem?.AdWasWatched().

Balancy.Actions.Ads.SetAdWatchCallback((callback) => {
    //TODO Implement your ad watch logic here
    callback(true);
});
Balancy.Actions.Ads.setAdWatchCallback((storeItem : SmartObjectsStoreItem) => {
    console.log('Fake ad watched for:', storeItem?.name);
    //TODO Implement your ad watch logic here
    storeItem?.adWasWatched();
});

Cloud Storage

Store and read your own key/value data in the cloud, organized into named collections. This is a general-purpose, per-player cloud key/value store you control directly from your game code — separate from Balancy's built-in Configs & Profiles and inventory.

Typical uses:

  • Saving custom progress or settings that don't fit the built-in profile.
  • Cross-device state you want to read/write on demand.
  • Small pieces of server-synced data (flags, timestamps, counters, JSON blobs).

Concepts

  • Collection — a named bucket of key/value pairs (e.g. "settings", "progress"). You choose the names; they're created on first write.
  • Key — a string identifier within a collection (e.g. "score", "lastLevel").
  • Value — an arbitrary JSON value: a number, string, boolean, object, or array. You pass values as JSON strings.
  • Cloud-only — every call goes straight to the server. There is no local cache: a read always fetches the current cloud state, and a write is sent immediately.
  • Asynchronous — every operation completes via a callback. The callback is invoked on the main thread.
  • Per player — data is scoped to the currently authenticated player.

Values are raw JSON

To store the string hello, pass "\"hello\"" (a quoted JSON string), not "hello". To store the number 42, pass "42". To store an object, pass "{\"sound\":true}".

Writing invalid JSON fails with an error in ErrorMessage/errorMessage.

Response object

Every callback receives a response with:

  • Success/success (bool): true if the operation succeeded.
  • ErrorCode/errorCode (int): Error code when the operation failed.
  • ErrorMessage/errorMessage (string): Human-readable error when the operation failed.
  • Data/data (string): (reads only) the JSON payload — a value or a whole collection.
  • Version/version (int): (reads only) the stored version of the key (-1 if absent).

Write callbacks only carry Success / ErrorCode / ErrorMessage.

Write a Single Key

Writes one key to a collection.

Balancy.API.CloudStorage.SetValue(
    "progress",           // collection
    "lastLevel",          // key
    "12",                 // value (JSON)
    response => {
        if (response.Success)
            Debug.Log("Saved");
        else
            Debug.LogWarning("Save failed: " + response.ErrorMessage);
    });
Balancy.API.CloudStorage.setValue("progress", "lastLevel", "12", response => {
    if (response.success) console.log("Saved");
    else console.warn("Save failed:", response.errorMessage);
});

The callback is optional — you can fire-and-forget:

Balancy.API.CloudStorage.SetValue("progress", "lastLevel", "12");
Balancy.API.CloudStorage.setValue("progress", "lastLevel", "12");

Write Several Keys at Once

Writes multiple keys in a single request.

var values = new Dictionary<string, string> {
    ["score"]    = "1500",
    ["settings"] = "{\"sound\":true,\"music\":false}",
    ["name"]     = "\"Player One\"",
};

Balancy.API.CloudStorage.SetValues("progress", values, response => {
    Debug.Log(response.Success ? "All saved" : response.ErrorMessage);
});
Balancy.API.CloudStorage.setValues("progress", {
    score: "1500",
    settings: '{"sound":true,"music":false}',
    name: '"Player One"',
}, response => {
    console.log(response.success ? "All saved" : response.errorMessage);
});

Read a Single Key

Reads one key. On success, Data holds the key's JSON value (an empty string if the key doesn't exist), and Version holds its stored version (-1 if absent).

Balancy.API.CloudStorage.GetValue("progress", "lastLevel", response => {
    if (response.Success)
        Debug.Log($"lastLevel = {response.Data} (version {response.Version})");
    else
        Debug.LogWarning("Read failed: " + response.ErrorMessage);
});
Balancy.API.CloudStorage.getValue("progress", "lastLevel", response => {
    if (response.success)
        console.log(`lastLevel = ${response.data} (version ${response.version})`);
    else
        console.warn("Read failed:", response.errorMessage);
});

Prefer reading the whole collection

Reading one key fetches the collection server-side and extracts that key. If you need several keys, use GetCollection/getCollection instead — it's a single request for all of them.

Read a Whole Collection

Reads every key in a collection. On success, Data is a JSON object mapping each key to its value: {"score":1500,"settings":{...},"name":"Player One"}.

Version is -1 for a whole-collection read — per-key versions are only returned by a single-key read.

Balancy.API.CloudStorage.GetCollection("progress", response => {
    if (response.Success)
        Debug.Log("collection = " + response.Data);   // JSON object
});
Balancy.API.CloudStorage.getCollection("progress", response => {
    if (response.success)
        console.log("collection =", response.data);    // JSON object
});

Versioning (Optimistic Concurrency)

Every stored key has a version. Versioning lets you avoid overwriting data that changed since you last read it — for example, when the same player is active on two devices at once.

How it works:

  1. Read a key with GetValue. The response includes the current Version.
  2. Modify the value locally.
  3. Write it back passing the version you read. The server accepts the write only if the stored version still matches; otherwise the write fails (someone else changed the value in the meantime).

The version parameter is optional and is the last parameter on the write methods, defaulting to -1:

  • -1 (the default) = no version check. The write always overwrites the current value. This is the normal behavior when you don't pass a version.
  • 0 or greater = validate. The value is written only if the stored version equals the one you passed.

0 is a valid version

Only a negative version (-1) means "skip the check". 0 is treated as a genuine version to validate against — it is not ignored.

Example: Safe Read-Modify-Write

Balancy.API.CloudStorage.GetValue("progress", "score", read => {
    if (!read.Success) return;

    int score = int.Parse(string.IsNullOrEmpty(read.Data) ? "0" : read.Data);
    score += 100;

    // Write only if nobody changed "score" since we read it.
    Balancy.API.CloudStorage.SetValue("progress", "score", score.ToString(),
        write => {
            if (write.Success)
                Debug.Log("Score updated");
            else
                Debug.LogWarning("Conflict — re-read and retry: " + write.ErrorMessage);
        },
        version: read.Version);
});
Balancy.API.CloudStorage.getValue("progress", "score", read => {
    if (!read.success) return;

    const score = (read.data ? parseInt(read.data, 10) : 0) + 100;

    Balancy.API.CloudStorage.setValue("progress", "score", String(score),
        write => {
            if (write.success) console.log("Score updated");
            else console.warn("Conflict — re-read and retry:", write.errorMessage);
        },
        read.version);
});

Versions on Batch Writes

SetValues/setValues accept an optional per-key versions map (also the last parameter). Only keys present in the map are version-checked; the rest are written unconditionally.

var values   = new Dictionary<string, string> { ["score"] = "1600", ["combo"] = "3" };
var versions = new Dictionary<string, int>    { ["score"] = 7 };   // check "score" only

Balancy.API.CloudStorage.SetValues("progress", values, response => {
    Debug.Log(response.Success ? "Saved" : response.ErrorMessage);
}, versions);
Balancy.API.CloudStorage.setValues("progress",
    { score: "1600", combo: "3" },
    response => console.log(response.success ? "Saved" : response.errorMessage),
    { score: 7 });   // check "score" only

Notes & Limits

  • No delete. There is currently no operation to remove a key or collection. Writing an empty/null value keeps the key in the collection.
  • Single-key read cost. Reading one key fetches the collection and extracts that key. If you need several keys, prefer GetCollection (one request).
  • Missing key. Reading a key that doesn't exist succeeds with an empty Data and a Version of -1.
  • Valid JSON required. Writing invalid JSON as a value fails with an error in ErrorMessage. Make sure values are well-formed JSON.
  • Authentication. Data is tied to the current player, so the SDK must be initialized and the player authenticated before these calls succeed.