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)¶
- Make sure you have installed the Unity's In App Purchasing package version 4.12+.
- Install the latest Balancy Payments package: Balancy Purchasing.
- Add
Balancy.Paymentsasmdef into your project assembly definition. - To avoid conflicts, make sure you are not invoking
UnityPurchasing.Initializein your code.
How it works under the hood:
- When
Balancy.Callbacks.OnDataUpdatedis invoked, Balancy automatically grabs all Products you defined in the Balancy dashboard and callsUnityPurchasing.Initializewith them. - 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.
- The purchase result is returned via the callback you provided.
-
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 succeededErrorCode/errorCode: Error code if operation failedErrorMessage/errorMessage: Error message if operation failedProduct/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
DateTimewithDateTimeKind.Utc. If you pass a local time, it will be converted viaToUniversalTime(), 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 callResetCustomUTCTimeProvider. - You can set the provider before calling
Init()— it will be used immediately. SetTimeCheatingOffsetoffset 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):trueif 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 (-1if 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:
- Read a key with
GetValue. The response includes the currentVersion. - Modify the value locally.
- 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.0or 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/
nullvalue 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
Dataand aVersionof-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.