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). Rounded to cents. A spend so small that INVO's fee would round to zero is refused with 400, error_code: "AMOUNT_BELOW_MINIMUM" (only when your fee rate is above zero). |
| 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": "95.00",
"platform_fee": "5.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 (95% to you, 5% 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
Server-side only. This call takes your title's X-Game-Secret-Key, which must never ship in a game or app client (Unity, Unreal, Godot, web or mobile). Your client asks your server, and your server makes this call. See Credentials & Key Handling.
Node.js (server)
// Node (server). INVO_GAME_SECRET comes from your server's environment.
const res = await fetch('https://invo.network/api/item-purchases/purchase-item', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Game-Secret-Key': process.env.INVO_GAME_SECRET,
},
body: JSON.stringify({
client_request_id: crypto.randomUUID(), // reuse the same id if you retry
player_email: 'player@example.com',
player_name: 'PlayerOne',
item_id: 'legendary_sword_001',
item_name: 'Legendary Sword',
item_quantity: 1,
unit_price: '100.00',
total_price: '100.00',
}),
});
const data = await res.json();
// 200: grant the item, then show data.new_balance.Python (server)
# Python (server)
import os, uuid, requests
r = requests.post(
"https://invo.network/api/item-purchases/purchase-item",
headers={"X-Game-Secret-Key": os.environ["INVO_GAME_SECRET"]},
json={
"client_request_id": str(uuid.uuid4()),
"player_email": "player@example.com",
"player_name": "PlayerOne",
"item_id": "legendary_sword_001",
"item_name": "Legendary Sword",
"item_quantity": 1,
"unit_price": "100.00",
"total_price": "100.00",
},
timeout=15,
)
data = r.json()
# 200: grant the item, then show data['new_balance'].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=50&offset=0 (limit defaults to 50, at most 100)
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": 50,
"offset": 0,
"has_more": false
}
}/api/item-purchases/order-details
Retrieves the complete financial record for a single purchase order.
Query Parameters: exactly one of ?order_id=ord_..., ?transaction_id=txn_... or ?client_request_id=... (your own idempotency key, useful when a purchase call timed out and you need to know what happened)
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.
- Purchases may be temporarily refused after repeated failed attempts (for example repeated insufficient-balance or unknown-item attempts), rapid repeated attempts, or repeated malformed requests.
- The refusal is a
429whose body says when to retry (retry_after, in seconds). Wait that long, and fix the cause (check the balance or the item) before trying again. - An insufficient balance itself is a
400withmessage,required_amountandcurrent_balance, and noerror_code: branch on the status and those fields.