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). 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_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...",
  "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

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

/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 429 whose 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 400 with message, required_amount and current_balance, and no error_code: branch on the status and those fields.