Item Purchase API
Allow players to purchase in-game items using their existing currency balances. This API provides a secure, transactional method for managing your in-game economy.
Note on minor accounts: item purchases are intentionally not gated by guardian approval, even when the buyer is flagged as a minor. The guardian's approval was given when the currency entered the wallet (via a guardian-approved transfer or send); a second gate at spend time is redundant friction. Minor users can purchase items normally. See Guardian Approval for the full scope.
Purchase Item
The core endpoint to execute an item purchase.
POST /api/item-purchases/purchase-itemPurchase History
Retrieve a specific player's item purchase history.
GET /api/item-purchases/player-purchase-historyOrder Details
Get detailed financial records for a specific order.
GET /api/item-purchases/order-details1. Purchase Item Endpoint
/api/item-purchases/purchase-item
Securely processes an item purchase by deducting from a player's balance.
Security Warning
This endpoint must only be called from your secure game server. Your server should first validate the player's session and then make this API call. Never call it directly from a game client.
Authentication Required
All requests to this endpoint must include your game's secret key in the request headers:
X-Game-Secret-Key: your_game_secret_key_hereYou can find your game's secret key in your Invo dashboard under Game Settings → API Keys.
Request Body Parameters
| Parameter | Type | Description |
|---|---|---|
| client_request_id | string | A unique ID generated by your client to ensure idempotency. See section below. |
| player_email | string | The email address of the purchasing player. |
| player_name | string | The player's current display name. |
| item_id | string | Your internal unique identifier for the item. |
| item_name | string | The display name of the item. |
| item_quantity | integer | The number of units being purchased. |
| unit_price | string | The price of a single unit of the item, as a string. |
| total_price | string | The total price for the purchase (quantity * unit_price). |
| item_category | string (optional) | A category for the item (e.g., "Weapons", "Skins"). |
| item_description | string (optional) | Free-form description for receipts and history views. |
| player_phone | string (optional) | E.164 phone number; recorded if the player auto-creates on this call. |
Successful response (HTTP 200)
{
"status": "success",
"message": "Item purchased successfully",
"transaction_id": "TXN...",
"order_id": "ORD...",
"new_balance": "900.00",
"previous_balance": "1000.00",
"currency_name": "Gold Coins",
"purchase_details": {
"item_id": "legendary_sword_001",
"item_name": "Legendary Sword",
"item_description": "A powerful weapon",
"item_category": "weapons",
"quantity": 1,
"unit_price": "100.00",
"total_price": "100.00",
"currency_name": "Gold Coins",
"purchased_by": "Player Name"
},
"balance_info": {
"previous_balance": "1000.00",
"amount_spent": "100.00",
"new_balance": "900.00"
},
"financial_breakdown": {
"total_paid": "100.00",
"developer_revenue": "90.00",
"platform_fee": "10.00"
},
"player": {
"player_id": 102,
"player_email": "player@example.com",
"player_name": "Player Name"
}
}Read the player's spendable balance from the top-level new_balance. It is the canonical post-write value (the same number is mirrored in balance_info.new_balance for legacy callers). Treat it as eventually-consistent for batch reads. financial_breakdown reflects the network's current item-purchase fee split (90% to you, 10% network fee).
2. Idempotency and Safe Retries
Preventing Duplicate Purchases
Network errors can happen. To prevent a player from being charged twice for the same item if a request is retried, you must provide a unique client_request_id for every purchase attempt.
If our server receives a request with a client_request_id it has already processed, it will not create a new transaction. Instead, it will return a 409 Conflict error, letting you know the purchase is already complete. A good practice is to generate a UUID on your client for each new purchase action.
Reserved prefix: sub_
A client_request_id beginning with sub_ (case-insensitive) is rejected with HTTP 400, error_code: "CLIENT_REQUEST_ID_RESERVED". That prefix is reserved for the renewal charges behind subscriptions, which share this idempotency key space. See renewals. A partner value parked under that prefix could collide with a future renewal charge.
What to check: identifiers derived from something you did not generate yourself: a user-supplied string, an item SKU, an upstream order ID. UUIDs are unaffected.
Error codes: match on error_code_public
Item purchases are settled entirely against a player's existing balance, so no card rail is involved and the processor-related error codes never appear here. Where they can appear (currency purchases and the card leg of platform commerce), responses now carry a provider-neutral error_code_public alongside the original error_code. Match on error_code_public; the change is additive and existing integrations matching error_code are unaffected. The full alias table is on the Currency Purchase page.
3. Code Implementation Examples
UnityUnity C# (Server-Side Logic)
// This function MUST be on your secure server.
public IEnumerator PurchaseItem(string playerEmail, string itemName, string itemId, int quantity, float price, Action<string> onSuccess, Action<string> onError)
{
string url = "https://invo.network/api/item-purchases/purchase-item";
var requestData = new {
client_request_id = System.Guid.NewGuid().ToString(), // Generate a unique ID for each attempt
player_email = playerEmail,
player_name = "Player fetched from your DB",
item_id = itemId,
item_name = itemName,
item_quantity = quantity,
unit_price = price.ToString("F2"),
total_price = (price * quantity).ToString("F2")
};
string jsonBody = JsonUtility.ToJson(requestData);
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("X-Game-Secret-Key", "your_game_secret_key_here");
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success) {
onSuccess?.Invoke(request.downloadHandler.text);
} else {
onError?.Invoke(request.downloadHandler.text);
}
}
}UnrealUnreal C++ (Server-Side Logic)
// This function MUST be on your secure server.
void AYourGameBackend::PurchaseItem(const FString& PlayerEmail, const FString& ItemId, int32 Quantity, float Price)
{
TSharedPtr<FJsonObject> RequestObj = MakeShareable(new FJsonObject);
RequestObj->SetStringField("client_request_id", FGuid::NewGuid().ToString());
RequestObj->SetStringField("player_email", PlayerEmail);
RequestObj->SetStringField("player_name", "Player Name from DB");
RequestObj->SetStringField("item_id", ItemId);
RequestObj->SetStringField("item_name", "Item Name from DB");
RequestObj->SetNumberField("item_quantity", Quantity);
RequestObj->SetStringField("unit_price", FString::Printf(TEXT("%.2f"), Price));
RequestObj->SetStringField("total_price", FString::Printf(TEXT("%.2f"), Price * Quantity));
FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(RequestObj.ToSharedRef(), Writer);
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL("https://invo.network/api/item-purchases/purchase-item");
Request->SetVerb("POST");
Request->SetHeader("X-Game-Secret-Key", "your_game_secret_key_here");
Request->SetHeader("Content-Type", "application/json");
Request->SetContentAsString(RequestBody);
Request->OnProcessRequestComplete().BindUObject(this, &AYourGameBackend::OnPurchaseItemResponse);
Request->ProcessRequest();
}GodotGodot GDScript (Server-Side Logic)
# This function MUST be on your secure server.
# Assumes you have an HTTPRequest node named 'http_request'.
# You can use OS.execute("uuidgen") on supported platforms or other methods for unique IDs.
func purchase_item(player_email: String, item_id: String, quantity: int, price: float):
var url = "https://invo.network/api/item-purchases/purchase-item"
var headers = [
"X-Game-Secret-Key: your_game_secret_key_here",
"Content-Type: application/json"
]
var body = {
"client_request_id": UUID.new().uuid_string(), # Requires a UUID plugin or custom function
"player_email": player_email,
"player_name": "Player From DB",
"item_id": item_id,
"item_name": "Item From DB",
"item_quantity": quantity,
"unit_price": "%.2f" % price,
"total_price": "%.2f" % (price * quantity)
}
http_request.request(url, headers, HTTPClient.METHOD_POST, JSON.stringify(body))
4. Helper Endpoints
/api/item-purchases/player-purchase-history
Retrieves a paginated list of items a player has purchased.
Query Parameters: ?player_email=player@example.com&limit=25&offset=0
Example Response:
{
"player": { /* player details */ },
"item_purchase_history": [
{
"transaction_id": "txn_...",
"status": "completed",
"amount": "150.00",
"created_at": "2024-06-21T14:30:00Z",
"item_id": "legendary_sword_001",
"item_name": "Legendary Sword",
"item_quantity": 1
}
],
"pagination": {
"total_count": 1,
"limit": 25,
"offset": 0,
"has_more": false
}
}/api/item-purchases/order-details
Retrieves the complete financial record for a single purchase order.
Query Parameters: ?order_id=ord_... OR ?transaction_id=txn_...
Example Response:
{
"order": { /* ... full order object ... */ },
"financial_summary": {
"game_currency_spent": "150.00",
"payment_method": "in_game_currency"
},
"status_timeline": {
"created_at": "2024-06-21T14:30:00Z",
"completed_at": "2024-06-21T14:30:00Z"
}
}5. Advanced Security Features
Automated Fraud & Spam Protection
The API includes several automated security checks to protect your game's economy from abuse. If a player triggers one of these checks, they will receive a 429 Too Many Requests error with a message explaining the block.
- Insufficient-balance lockout: 50 failed purchase attempts due to insufficient funds within 5 minutes triggers a 5-minute cooldown.
- Rapid-same-item lockout: More than 10 purchase attempts for the same item within 1 minute triggers a temporary block.
- Per-player spam lockout: Roughly 100 purchase attempts within a rolling 5-minute window triggers a cooldown (a secondary guard adds a wider 200-attempt limit). Expect to be told to retry in up to 15 minutes.
- Invalid-item lockout: 30 attempts referencing an unrecognized item within the window triggers a 10-minute cooldown.
- Malformed-request lockout: 50 invalid requests from the same network triggers a 30-minute cooldown.
- All 429 responses include a
retry_afterfield (seconds). Back off accordingly.