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-item

Purchase History

Retrieve a specific player's item purchase history.

GET /api/item-purchases/player-purchase-history

Order Details

Get detailed financial records for a specific order.

GET /api/item-purchases/order-details

1. Purchase Item Endpoint

POST

/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_here

You can find your game's secret key in your Invo dashboard under Game Settings → API Keys.

Request Body Parameters

ParameterTypeDescription
client_request_idstringA unique ID generated by your client to ensure idempotency. See section below.
player_emailstringThe email address of the purchasing player.
player_namestringThe player's current display name.
item_idstringYour internal unique identifier for the item.
item_namestringThe display name of the item.
item_quantityintegerThe number of units being purchased.
unit_pricestringThe price of a single unit of the item, as a string.
total_pricestringThe total price for the purchase (quantity * unit_price).
item_categorystring (optional)A category for the item (e.g., "Weapons", "Skins").
item_descriptionstring (optional)Free-form description for receipts and history views.
player_phonestring (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_...",
  "purchase_details": {
    "item_id": "legendary_sword_001",
    "item_name": "Legendary Sword",
    "quantity": 1,
    "total_price": "100.00",
    "currency_name": "Gold Coins"
  },
  "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"
  }
}

new_balance is authoritative for the moment of write — 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.

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

GET

/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
  }
}
GET

/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: More than 200 total purchase attempts within 5 minutes triggers a 5-minute cooldown.
  • All 429 responses include a retry_after field (seconds) — back off accordingly.