With ZEPETO Product systems, World Creators can earn profits by building economic systems and selling items.
Caution
- This feature are available in ZEPETO World Package 1.12.2 and later versions. If the World version is older, please proceed with the World update first.
- For previously implemented worlds using IWP components, it is recommended to use IWP's higher compatible Product Packages.
- The IWP package is scheduled to be discontinued in October 2023. If there are worlds using IWP, please update them using the Product package and release them as a new version.
STEP 1 : Setting up a product at ZEPETO Studio
STEP 1-1 : Registering a World ID
The first thing you need to do is issue a World ID at ZEPETO Studio and then proceed with the product registration.
Also, you must register a world cover image.

Check out the link below for more information.
Please refer to the following guide. [Making a World Console]
STEP 1-2 : Registering Currencies
There are two main types of currencies available in ZEPETO World.
- ZEPETO Official ZEM
- This is a currency that can be charged and used for a fee.
- Custom Currency
- Currency that can only be used in the world.
- Custom Currency can be created and managed in ZEPETO Studio.
- Custom Currency can be registered as a 'Currency Package' product in ZEPETO Studio and sold as a ZEM product.
Please refer to the following guide. [World Currency]
STEP 1-3 : Register an item
Please register the items you want to use in the inventory in ZEPETO Studio.
Items can be sold as ZEM and my own currency.
Please refer to the following guide. [World Products]
STEP 2 : Installing the ZEPETO.Product Package
Install Window → Package Manager → ZEPETO.Product
ZEPETO Multiplay Server must be added to work with ZEPETO Studio.
Select ZEPETO → Multiplay Server on the [+] menu at the top left of the [Project] panel, or go to Assets → Create → ZEPETO → Multiplay Server.
Caution
- You don't have to do any schema work to use ZEPETO products.
- Instead, server scripts and client scripts need to be implemented to communicate with the server.
- Please refer to the ZEPETO Product sample and the ZEPETO Product API for implementation of the script.
STEP 3 : ZEPETO Product Sample
The following is a sample implementation of ZEPETO Product.
To understand the ZEPETO Product system, we recommend that you follow the guide to understand the implementation of the sample.
Samples include examples of currency systems, inventory systems, item acquisition, purchase, and UI resources.

ZEPETO Product Sample UI
Caution
- Please feel free to use the UI resources and scripts provided in the sample when you create the ZEPETO World.
- However, it is prohibited to use the resources of the published samples other than ZEPETO World.
ZEPETO Product Sample
How to apply a sample of ZEPETO Product
- Please register the currency and products with the same name as those used in the sample at ZEPETO Studio.(If the currency ID and product ID are different, they cannot be linked with the sample.)
- Currency
ID | Currency Name |
---|---|
star | star |
energy | energy |
- Item
ID | Name | Sales Currency | Price | Type |
---|---|---|---|---|
note1 | note1 | star | 10 | Nonconsumables |
potion4 | potion4 | energy | 4 | Consumables |
potion3 | potion3 | energy | 3 | Consumables |
potion2 | potion2 | energy | 2 | Consumables |
potion1 | potion1 | energy | 1 | Consumables |
- Currency Package
ID | Name | Sales Currency | Price | Package Configuration |
---|---|---|---|---|
energy4 | energy4 | ZEPETO ZEM | 4 | energy, 40 |
energy3 | energy3 | ZEPETO ZEM | 3 | energy, 30 |
energy2 | energy2 | ZEPETO ZEM | 2 | energy, 20 |
energy1 | energy1 | ZEPETO ZEM | 1 | energy, 10 |
- Item Package
ID | Name | Sales Currency | Price | Package Configuration(item) |
---|---|---|---|---|
potion_package | potion_package | ZEPETO ZEM | 3 | potion4, 3 |

Package configuration example
- Import Product Canvas located in Assets/ProductSystem/Prefab into Scene.
- Enter your World ID in Open World Setting.

- Run Unity menu bar > ZEPETO > Product > Settings.
- If the currencies and product list are displayed well, the link will be successful.

- You can verify that the icon image in the Assets>Product>Resources folder is set.

- You can register the cover photo of the product directly.

Caution
- If you can't see the product properly, please check the following.
- Please make sure that the account information logged in to the Unity Editor is the same as the World id creator's information.
- Please verify that the WorldId information entered in World Settings is correct.
- Please confirm that you have registered the same id currencies and products as those used in the sample.
- Please turn on the multi-server and press the Play button to run the sample.
STEP 4 : ZEPETO Product Sample Function Instruction
◦ Gain Energy : When you click a button, you get one energy product.
◦ Use Energy : When you click a button, you can deduct one energy item
◦ Increase EXP : Click the button to obtain experience value by 10. If you get an experience value and your level goes up, you get 5 Star currency.
◦ Acquire Random Item : Click the button to randomly add one of the four potion items to the inventory.
◦ Purchase Immediately : When you click the button, you can deduct one energy item and purchase Potion 1. The purchase window does not appear, and the purchased items are immediately in the inventory.
◦ Purchase through UI : Click the button and the purchase window will appear.You can purchase Note 1 by deducting 10 Star currency from the purchase window.
◦ Purchase ItemPackage : Click the button and the purchase window will appear.You can purchase potion package products by deducting ZEPETO ZEM from the purchase window.
- Bag icon: Click on the button to display the inventory window. You can check the list of items you have, select them, and use them.
- Shopping cart icon: When you click the button, the purchase window for energy currency appears. You can purchase currency package products by deducting ZEPETO ZEM from the purchase window.
STEP 5 : Precautions for ZEPETO Product Test
- When multi-server is turned on, ZEPETO Product testing is possible on the sandbox server.
- The changes made in the editor will not be reflected in ZEPETO Studio.
- Changes made to Pretest and released World will be reflected in ZEPETO Studio.
- Sandbox test environment will be initialized when multi-server is turned off.(It will be initialized with ZEPETO studio data.)
- Testing non-consumable items
- If the multi-server is on, you cannot purchase it again once you purchase it.
- If you turn off the multi-server, the test environment will be initialized, so if you turn on the multi-server again, you can purchase it.
- Without registering a World Cover Image, you cannot properly perform Product testing. Please make sure to check whether the World Cover Image has been correctly registered in ZEPETO Studio.

Without registering a World Cover Image, you cannot properly perform Product testing
STEP 6 : Implement content after purchasing a product
Samples do not implement content after the item is used.
That part must be implemented directly as intended by the world's producers.
Please refer to the script in UICommonBtn.ts.
this._multiplay.Room.AddMessageHandler<InventorySync>("SyncInventories", (message) => {
this.OpenInformation(`${message.productId} has been ${InventoryAction[message.inventoryAction]} in the inventory.`);
// Item use sample
if(message.inventoryAction == InventoryAction.Used){
if(message.productId == "potion1"){
console.log("potion use!");
}
}
});
The key is to implement the content code according to the product ID when using the product.
If you want to implement when using potion2, make the following changes:
this._multiplay.Room.AddMessageHandler<InventorySync>("SyncInventories", (message) => {
this.OpenInformation(`${message.productId} has been ${InventoryAction[message.inventoryAction]} in the inventory.`);
// Item use sample
if(message.inventoryAction == InventoryAction.Used){
if(message.productId == "potion2"){
console.log("potion2 use!");
}
}
});
STEP 7 : Using Product Purchase Button
In addition to the buttons provided by the sample, you can easily add the direct purchase button.
Click Hierarchy > ZEPETO > Product Purchase Buton to automatically create a button.
You can choose which product you want to link with on the button inspector.
Please select the product id you want to work with.

Please turn on the multi-server and press the Play button to run the sample.
If you click the button, you can see the sales window automatically.
If you click the button after linking other products, you can see the sales window of the linked products automatically.

Products purchased through Product Purchase Buton can also be checked in the inventory.
STEP 8 : ZEPETO Product Sample Script
In order to build an economic system using ZEPETO Products, it is important to implement client-server scripts correctly.
Please refer to the example script implemented in the ZEPETO Product Sample.
Please refer to the following guide for the ZEPETO.Product Package API.
Please refer to the following guide. [ZEPETO Product API]
script showing user currencies defined at the top of UI
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Text, Button, InputField, Slider } from 'UnityEngine.UI';
import { Object, WaitForSeconds, WaitUntil } from 'UnityEngine';
import { InventoryService } from "ZEPETO.Inventory";
import { BalanceListResponse, CurrencyService, CurrencyError } from "ZEPETO.Currency";
import { ProductRecord, ProductService } from "ZEPETO.Product";
import { ZepetoWorldMultiplay } from "ZEPETO.World";
import { RoomData, Room } from "ZEPETO.Multiplay";
export default class UIBalances extends ZepetoScriptBehaviour {
@SerializeField() private possessionStarTxt : Text;
@SerializeField() private possessionEnergyTxt : Text;
@SerializeField() private possessionZemTxt : Text;
@SerializeField() private possessionExpTxt : Text;
@SerializeField() private possessionAmountExpTxt : Text;
@SerializeField() private possessionLevelTxt : Text;
@SerializeField() private expSlider : Slider;
private _multiplay : ZepetoWorldMultiplay;
private _room : Room;
private _myExp :number = 0;
private _amountExp :number = 30;
private _myLevel:number = 1;
private Start() {
this.RefreshAllBalanceUI();
this.RefreshExpUI();
this._multiplay = Object.FindObjectOfType<ZepetoWorldMultiplay>();
this._multiplay.RoomJoined += (room: Room) => {
this._room = room;
this.InitMessageHandler();
}
}
private InitMessageHandler() {
this._room.AddMessageHandler<BalanceSync>("SyncBalances", (message) => {
this.RefreshAllBalanceUI();
});
ProductService.OnPurchaseCompleted.AddListener((product, response) => {
this.RefreshAllBalanceUI();
});
}
private RefreshAllBalanceUI(){
this.StartCoroutine(this.RefreshBalanceUI());
this.StartCoroutine(this.RefreshOfficialCurrencyUI());
}
private *RefreshBalanceUI(){
const request = CurrencyService.GetUserCurrencyBalancesAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if(request.responseData.isSuccess) {
this.possessionStarTxt.text = request.responseData.currencies?.ContainsKey(Currency.star) ? request.responseData.currencies?.get_Item(Currency.star).toString() :"0";
this.possessionEnergyTxt.text = request.responseData.currencies?.ContainsKey(Currency.energy) ? request.responseData.currencies?.get_Item(Currency.energy).toString() :"0";
}
}
private *RefreshOfficialCurrencyUI(){
const request = CurrencyService.GetOfficialCurrencyBalanceAsync();
yield new WaitUntil(() => request.keepWaiting == false);
this.possessionZemTxt.text = request.responseData.currency.quantity.toString();
}
public IncreaseExp(quantity:number){
this._myExp += quantity;
if(this._myExp >= this._amountExp){
this._myLevel++;
this._myExp -= this._amountExp;
this.LevelUpReward();
}
this.RefreshExpUI();
}
private RefreshExpUI(){
this.possessionExpTxt.text = this._myExp.toString();
this.possessionLevelTxt.text = this._myLevel.toString();
this.expSlider.value = this._myExp / this._amountExp;
}
private LevelUpReward(){
// Get 5 stars for every level you raise
const data = new RoomData();
data.Add("currencyId", Currency.star);
data.Add("quantity", 5);
this._multiplay.Room?.Send("onCredit", data.GetObject());
}
}
export interface BalanceSync {
currencyId: string,
quantity: number,
}
export interface InventorySync {
productId: string,
inventoryAction: InventoryAction,
}
export enum InventoryAction{
Removed = -1,
Used,
Added,
}
export enum Currency{
star = "star",
energy = "energy",
}
When the RefreshAllBalanceUI() function first enters the world, it uses CurrentService.GetUserCurrentValancesAsync() to receive custom currency information and ZEM official information using CurrentService.GetOfficialCurrencyAsync().
A script for displaying an item list in the inventory UI and using the item.
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Button, Text, ToggleGroup } from 'UnityEngine.UI';
import { GameObject, Object, Sprite, Transform, WaitUntil } from 'UnityEngine';
import { InventoryRecord, InventoryService } from "ZEPETO.Inventory";
import { CurrencyService } from "ZEPETO.Currency";
import { ProductRecord, ProductService, PurchaseType } from "ZEPETO.Product";
import { ZepetoWorldMultiplay } from "ZEPETO.World";
import { Room, RoomData } from "ZEPETO.Multiplay";
import ITM_Inventory from './ITM_Inventory';
import { BalanceSync, InventorySync, Currency } from "./UIBalances";
export default class UIInventory extends ZepetoScriptBehaviour {
@SerializeField() private usedSlotNumTxt : Text;
@SerializeField() private possessionStarTxt : Text;
@SerializeField() private useBtn : Button;
@SerializeField() private contentParent : Transform;
@SerializeField() private prefItem : GameObject;
@SerializeField() private itemImage : Sprite[];
private _inventoryCache: InventoryRecord[];
private _productCache: Map<string, ProductRecord> = new Map<string, ProductRecord>();
private _multiplay : ZepetoWorldMultiplay;
private _room : Room;
private Start() {
this._multiplay = Object.FindObjectOfType<ZepetoWorldMultiplay>();
this._multiplay.RoomJoined += (room: Room) => {
this._room = room;
this.InitMessageHandler();
}
this.StartCoroutine(this.LoadAllItems());
}
private InitMessageHandler(){
ProductService.OnPurchaseCompleted.AddListener((product, response) => {
this.StartCoroutine(this.RefreshInventoryUI());
this.StartCoroutine(this.RefreshBalanceUI());
});
this._room.AddMessageHandler<InventorySync>("SyncInventories", (message) => {
this.StartCoroutine(this.RefreshInventoryUI());
});
this._room.AddMessageHandler<BalanceSync>("SyncBalances", (message) => {
this.StartCoroutine(this.RefreshBalanceUI());
});
this.useBtn.onClick.AddListener(()=> this.OnClickUseInventoryItem());
}
private *LoadAllItems() {
const request = ProductService.GetProductsAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if (request.responseData.isSuccess) {
request.responseData.products.forEach((pr) => {
this._productCache.set(pr.productId,pr);
});
}
this.StartCoroutine(this.RefreshInventoryUI());
this.StartCoroutine(this.RefreshBalanceUI());
}
private *RefreshInventoryUI(){
const request = InventoryService.GetListAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if(request.responseData.isSuccess) {
const items: InventoryRecord[] = request.responseData.products;
items.forEach((ir, index) => {
// If there are zero consumable items, delete them from the inventory.
if (ir.quantity <= 0 && this._productCache.get(ir.productId).PurchaseType == PurchaseType.Consumable) {
// Remove inventory
const data = new RoomData();
data.Add("productId", ir.productId);
this._multiplay.Room?.Send("onRemoveInventory", data.GetObject());
return;
}
});
// If the value matches the previously received value, do not update it.
if (this._inventoryCache === items)
return;
else if (items != null && this._inventoryCache?.length == items.length)
this.UpdateInventory(items);
else
this.CreateInventory(items);
this.usedSlotNumTxt.text = items.length.toString();
this._inventoryCache = items;
}
}
private UpdateInventory(items:InventoryRecord[]){
const itemScripts = this.contentParent.GetComponentsInChildren<ITM_Inventory>();
items.forEach((ir) => {
itemScripts.forEach((itemScript) => {
if(itemScript.itemRecord.productId == ir.productId) {
const isShowQuantity:boolean = this._productCache.get(ir.productId).PurchaseType == PurchaseType.Consumable;
itemScript.RefreshItem(ir, isShowQuantity);
return;
}
});
});
}
private CreateInventory(items :InventoryRecord[]){
this.contentParent.GetComponentsInChildren<ITM_Inventory>().forEach((child) => {
GameObject.Destroy(child.gameObject);
});
// Sort by Create Order (descending order)
items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
items.forEach((ir, index) => {
const itemObj = Object.Instantiate(this.prefItem, this.contentParent) as GameObject;
const itemScript = itemObj.GetComponent<ITM_Inventory>();
this.itemImage.forEach((s, index) => {
// Import by name comparison from image resources.
if (s.name == ir.productId) {
itemScript.itemImage.sprite = this.itemImage[index];
return;
}
});
// Non-consumable items do not display numbers.
const isShowQuantity:boolean = this._productCache.get(ir.productId).PurchaseType == PurchaseType.Consumable;
itemScript.RefreshItem(ir, isShowQuantity);
itemScript.isOn(index == 0);
});
}
private *RefreshBalanceUI(){
const request = CurrencyService.GetUserCurrencyBalancesAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if(request.responseData.isSuccess) {
this.possessionStarTxt.text = request.responseData.currencies.ContainsKey(Currency.star) ? request.responseData.currencies.get_Item(Currency.star).toString() : "0";
}
else{
console.log(request.responseData.ErrorCode);
}
}
private OnClickUseInventoryItem(){
const toggleGroup = this.contentParent.GetComponent<ToggleGroup>();
const item = toggleGroup.GetFirstActiveToggle()?.GetComponent<ITM_Inventory>().itemRecord;
if(item == null){
console.warn("no have item data");
return;
}
if(this._multiplay.Room == null){
console.warn("server disconnect");
return;
}
const data = new RoomData();
data.Add("productId", item.productId);
data.Add("quantity", 1);
this._multiplay.Room?.Send("onUseInventory", data.GetObject());
}
}
The RefreshInventoryUI() function is a role that uses InventoryService.GetListAsync() to receive information about items in the inventory and then reflects the list in the UI.
If the number of items changes during use or acquisition, it will be called.
The sorting method is freely implemented, but the sample is implemented to sort in order of Create.
Consumable items in the inventory can be used through the OnClickUseInventoryItem() function.
If there is a change in the number of items, please implement it by sending the change to the server through Room Message.
script that implements each UI button function in the sample
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Button, Text } from 'UnityEngine.UI';
import { GameObject, Object, WaitUntil, WaitForSeconds } from 'UnityEngine';
import { ProductRecord, ProductService, ProductType, PurchaseType } from "ZEPETO.Product";
import { ZepetoWorldMultiplay } from "ZEPETO.World";
import { Room, RoomData } from "ZEPETO.Multiplay";
import UIBallances, { BalanceSync, Currency, InventoryAction, InventorySync } from "./UIBalances";
export default class UICommonBtn extends ZepetoScriptBehaviour {
@SerializeField() private gainBalanceBtn: Button;
@SerializeField() private useBalanceBtn: Button;
@SerializeField() private increaseExpBtn: Button;
@SerializeField() private acquireRandomItemBtn: Button;
@SerializeField() private purchaseImmediatelyBtn: Button;
@SerializeField() private purchaseOfficialUIBtn: Button;
@SerializeField() private purchaseItemPackageBtn: Button;
@SerializeField() private informationPref: GameObject;
private _itemsCache: ProductRecord[] = [];
private _itemsPackageCache: ProductRecord[] = []
private _multiplay: ZepetoWorldMultiplay;
private _room : Room;
private _uiBallances: UIBallances;
private Start() {
this._multiplay = Object.FindObjectOfType<ZepetoWorldMultiplay>();
this._uiBallances = Object.FindObjectOfType<UIBallances>();
// Button Interval
let allBtns : Button[] = this.GetComponentsInChildren<Button>();
allBtns.forEach(btn => btn.onClick.AddListener(() => this.StartCoroutine(this.BtnInterval(btn))));
this.StartCoroutine(this.LoadAllItems());
this._multiplay.RoomJoined += (room: Room) => {
this._room = room;
this.InitMessageHandler();
}
}
private InitMessageHandler() {
// Button listener
this.gainBalanceBtn.onClick.AddListener(() => this.OnClickGainBalance(Currency.energy, 1));
this.useBalanceBtn.onClick.AddListener(() => this.OnClickUseBalance(Currency.energy, 1));
this.increaseExpBtn.onClick.AddListener(() => this.OnClickIncreaseExp());
this.acquireRandomItemBtn.onClick.AddListener(() => this.OnClickAcquireRandomItem());
// Sell items with id called potion1.
this.purchaseImmediatelyBtn.onClick.AddListener(() => this.StartCoroutine(this.OnClickPurchaseItemImmediately("potion1")));
this.purchaseOfficialUIBtn.onClick.AddListener(() => {
// The first non-consumable item is sold through the official ui.
const nonConsumableItem = this._itemsCache.find(ir => ir.PurchaseType === PurchaseType.NonConsumable);
if (nonConsumableItem) {
this.OnClickPurchaseItem(nonConsumableItem);
}
else{
this.OpenInformation(`Non-consumable product does not exist.`);
}
});
this.purchaseItemPackageBtn.onClick.AddListener(() => {
const packageItem = this._itemsPackageCache[0];
if (packageItem) {
this.OnClickPurchaseItem(packageItem);
}
else{
this.OpenInformation(`Item package product does not exist.`);
}
});
// Log message handler
this._room.AddMessageHandler<BalanceSync>("SyncBalances", (message) => {
this.OpenInformation(`${message.currencyId} a Increase or decrease: ${message.quantity}`);
});
this._multiplay.Room.AddMessageHandler<InventorySync>("SyncInventories", (message) => {
this.OpenInformation(`${message.productId} has been ${InventoryAction[message.inventoryAction]} in the inventory.`);
// Item use sample
/*if(message.inventoryAction == InventoryAction.Used){
if(message.productId == "potion1"){
console.log("potion use!");
}
}*/
});
this._room.AddMessageHandler<string>("DebitError", (message) => {
this.OpenInformation(message);
});
ProductService.OnPurchaseCompleted.AddListener((product, response) => {
this.OpenInformation(`${response.productId} Purchase Completed`);
});
ProductService.OnPurchaseFailed.AddListener((product, response) => {
this.OpenInformation(response.message);
});
}
private *LoadAllItems() {
const request = ProductService.GetProductsAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if (request.responseData.isSuccess) {
this._itemsCache = [];
request.responseData.products.forEach((pr) => {
if (pr.ProductType == ProductType.Item) {
this._itemsCache.push(pr);
}
if (pr.ProductType == ProductType.ItemPackage) {
this._itemsPackageCache.push(pr);
}
});
if (this._itemsCache.length == 0) {
console.warn("no Item information");
return;
}
}
else{
console.warn("Product Load Failed");
}
}
private OpenInformation(message:string){
const inforObj = GameObject.Instantiate(this.informationPref,this.transform.parent) as GameObject;
inforObj.GetComponentInChildren<Text>().text = message;
}
private OnClickGainBalance(currencyId: string, quantity: number) {
const data = new RoomData();
data.Add("currencyId", currencyId);
data.Add("quantity", quantity);
this._multiplay.Room?.Send("onCredit", data.GetObject());
}
private OnClickUseBalance(currencyId: string, quantity: number) {
const data = new RoomData();
data.Add("currencyId", currencyId);
data.Add("quantity", quantity);
this._multiplay.Room?.Send("onDebit", data.GetObject());
}
private OnClickIncreaseExp() {
// This is just a test code that you don't save.
this._uiBallances.IncreaseExp(10);
}
private OnClickAcquireRandomItem() {
if (this._itemsCache.length == 0) {
console.warn("Item cache has not yet been loaded.");
return;
}
// Choose one of the consumable items.
const consumableItem: ProductRecord[] = [];
this._itemsCache.forEach((pr)=>{
if(pr.PurchaseType == PurchaseType.Consumable)
consumableItem.push(pr);
});
const randNum = Math.floor(Math.random() * consumableItem.length);
const randItem = consumableItem[randNum];
const data = new RoomData();
data.Add("productId", randItem.productId);
data.Add("quantity", 1);
this._multiplay.Room?.Send("onAddInventory", data.GetObject());
}
// An immediate purchase
private *OnClickPurchaseItemImmediately(productId: string) {
const request = ProductService.PurchaseProductAsync(productId);
yield new WaitUntil(() => request.keepWaiting == false);
if (request.responseData.isSuccess) {
// Is purchase success
} else {
// Is purchase fail
}
}
// Open offical ui
private OnClickPurchaseItem(productRecord: ProductRecord) {
ProductService.OpenPurchaseUI(productRecord);
}
private *BtnInterval(btn:Button){
btn.interactable = false;
yield new WaitForSeconds(0.2);
btn.interactable = true;
}
}
This script is the most important script in ZEPETO Product Sample.
Please refer to how each API is called when each button is pressed.
Asynchronous functions with async (e.g.ProductService.GetProductsAsync())The field should be called as shown in the example below.
private *LoadAllItems() {
const request = ProductService.GetProductsAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if (request.responseData.isSuccess) {
...
}
else{
console.warn("Product Load Failed");
}
}
Shop UI script
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Button, Text } from 'UnityEngine.UI';
import { WaitUntil, GameObject } from 'UnityEngine';
import { CurrencyService } from "ZEPETO.Currency";
import { CurrencyPackageUnitRecord, ProductRecord, ProductService, ProductType } from "ZEPETO.Product";
import ITM_ShopProduct from './ITM_ShopProduct';
export default class UIShop extends ZepetoScriptBehaviour {
@SerializeField() private possessionZemTxt : Text;
@SerializeField() private shopProducts : GameObject[];
private _products : ProductRecord[];
private Start() {
this.StartCoroutine(this.RefreshZemUI());
this.StartCoroutine(this.RefreshProducts());
ProductService.OnPurchaseCompleted.AddListener((productRecord, response) => {
this.StartCoroutine(this.RefreshZemUI());
});
}
private *RefreshZemUI(){
const request = CurrencyService.GetOfficialCurrencyBalanceAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if(request.responseData.isSuccess) {
this.possessionZemTxt.text = request.responseData.currency.quantity.toString();
}
}
private *RefreshProducts(){
const request = ProductService.GetProductsAsync();
yield new WaitUntil(() => request.keepWaiting == false);
if (!request.responseData || !request.responseData.isSuccess) {
console.warn("Refresh Products Failed");
return;
}
let currencyPackageIndex = 0;
for (const product of request.responseData.products || []) {
if (product.ProductType === ProductType.CurrencyPackage && currencyPackageIndex < this.shopProducts.length) {
this.shopProducts[currencyPackageIndex].GetComponent<ITM_ShopProduct>().RefreshProduct(product);
currencyPackageIndex++;
}
}
}
}
Server Script
import { Sandbox, SandboxOptions, SandboxPlayer } from "ZEPETO.Multiplay";
import { DataStorage } from "ZEPETO.Multiplay.DataStorage";
import { loadCurrency } from "ZEPETO.Multiplay.Currency";
import { loadInventory } from "ZEPETO.Multiplay.Inventory";
import { loadDataStorage } from 'ZEPETO.Multiplay.DataStorage';
export default class extends Sandbox {
storageMap: Map<string, DataStorage> = new Map<string, DataStorage>();
constructor() {
super();
}
async onCreate(options: SandboxOptions) {
this.onMessage("onCredit", (client, message:CurrencyMessage) => {
console.log(`[onCredit]`);
const currencyId = message.currencyId;
const quantity = message.quantity ?? 1;
this.AddCredit(client, currencyId, quantity);
});
this.onMessage("onDebit", (client, message:CurrencyMessage) => {
console.log(`[onDebit]`);
const currencyId = message.currencyId;
const quantity = message.quantity ?? 1;
this.OnDebit(client, currencyId, quantity);
});
this.onMessage("onAddInventory", (client, message:InventoryMessage) => {
console.log(`[onAddInventory]`);
const productId = message.productId;
const quantity = message.quantity ?? 1;
this.AddInventory(client, productId, quantity);
});
this.onMessage("onUseInventory", (client, message:InventoryMessage) => {
console.log(`[onUseInventory]`);
const productId = message.productId;
const quantity = message.quantity ?? 1;
this.UseInventory(client, productId, quantity);
});
this.onMessage("onRemoveInventory", (client, message:InventoryMessage) => {
console.log(`[onRemoveInventory]`);
const productId = message.productId;
this.RemoveInventory(client, productId);
});
// DataStorage
this.onMessage("onSetStorage", (client, message:StorageMessage) => {
console.log(`[onSetStorage]`);
const key = message.key;
const value = message.value ?? "";
this.SetStorage(client, key, value);
});
this.onMessage("onGetStorage", (client, message:StorageMessage) => {
const key = message.key;
console.log(`[onGetStorage] ${key}`);
this.GetStorage(client, key);
});
}
async SetStorage(client: SandboxPlayer, key: string, value: string) {
try {
const dataStorage = await loadDataStorage(client.userId);
const result = await dataStorage.set(key, value);
client.send("onSetStorageResult", result);
}
catch (e)
{
console.log(`${e}`);
}
}
async GetStorage(client: SandboxPlayer, key: string) {
try {
const dataStorage = await loadDataStorage(client.userId);
const result = await dataStorage.get(key) as string;
if (result === undefined) {
// It is an empty string
client.send("onGetStorageResult", "");
} else
{
client.send("onGetStorageResult", result);
}
}
catch (e)
{
console.log(`${e}`);
}
}
async AddInventory(client: SandboxPlayer, productId: string, quantity: number) {
try {
const inventory = await loadInventory(client.userId);
await inventory.add(productId, quantity);
const inventorySync: InventorySync = {
productId : productId,
inventoryAction : InventoryAction.Add
}
client.send("SyncInventories",inventorySync);
console.log("success add");
}
catch (e)
{
console.log(`${e}`);
}
}
async UseInventory(client: SandboxPlayer, productId: string, quantity: number) {
try {
const inventory = await loadInventory(client.userId);
if (await inventory.use(productId, quantity) === true) {
const inventorySync: InventorySync = {
productId: productId,
inventoryAction: InventoryAction.Use
}
client.send("SyncInventories", inventorySync);
console.log("success use");
}
else{
console.log("use error");
}
}
catch (e)
{
console.log(`${e}`);
}
}
async RemoveInventory(client: SandboxPlayer, productId: string) {
try {
const inventory = await loadInventory(client.userId);
if (await inventory.remove(productId) === true) {
const inventorySync: InventorySync = {
productId: productId,
inventoryAction: InventoryAction.Remove
}
client.send("SyncInventories", inventorySync);
console.log("success rm");
}
else{
console.log("remove error");
}
}
catch (e)
{
console.log(`${e}`);
}
}
async AddCredit(client: SandboxPlayer, currencyId: string, quantity: number) {
try {
const currency = await loadCurrency(client.userId);
await currency.credit(currencyId, quantity);
const currencySync: CurrencyMessage = {
currencyId : currencyId,
quantity : quantity
}
client.send("SyncBalances",currencySync);
}
catch (e)
{
console.log(`${e}`);
}
}
async OnDebit(client: SandboxPlayer, currencyId: string, quantity: number) {
try {
const currency = await loadCurrency(client.userId);
if(await currency.debit(currencyId, quantity) === true) {
const currencySync: CurrencyMessage = {
currencyId: currencyId,
quantity: -quantity
}
client.send("SyncBalances", currencySync);
}
else{
//It's usually the case that there's no balance.
client.send("DebitError", "Currency Not Enough");
}
}
catch (e)
{
console.log(`${e}`);
}
}
onJoin(client: SandboxPlayer) {
}
onTick(deltaTime: number): void {
}
onLeave(client: SandboxPlayer, consented?: boolean) {
}
}
interface CurrencyMessage {
currencyId: string,
quantity?: number,
}
interface InventoryMessage {
productId: string,
quantity?: number,
}
interface StorageMessage {
key: string,
value?: string,
}
interface InventorySync {
productId: string,
inventoryAction: InventoryAction,
}
export enum InventoryAction{
Remove = -1,
Use,
Add,
}
Product Application Example
- Here is an example of an In-World Product applied to the official ZEPETO World Slime Party.
- The product items have been registered in ZEPETO Studio as follows:

- For consumable buff items that accelerate the growth of slimes for a certain period, the settings are as follows:

- When integrated into the actual in-world UI, you can purchase the registered items as follows.
- When the item is successfully purchased, the buff's effect is triggered.
