Player Management API
Essential player balance management APIs designed for games and platforms with 60M+ players. Optimized for high-volume operations with intelligent caching, circuit breakers, and streaming capabilities for massive scale.
Registering players and changing phone numbers
This page covers reading balances. To put a player on Invo, keep their phone number in step with yours, and handle a player token after a phone change, see Player Identity.
- β’ Register a player with
POST /api/sdk/playersas soon as you have their email and phone. No purchase is needed. - β’ When a player changes their phone in your game, call
POST /api/sdk/players/phonebefore saving it locally. If the player has secured their account, Invo asks you to create a change request and the player confirms it on Invoβs page.
Individual Player Lookup
Get player balance by email with 5-minute caching and 2,000/minute rate limits for high-volume titles.
Batch Operations
Process up to 100K players per request with memory-efficient chunked processing and optimized queries.
Full Player Streaming
Stream all players for data exports and analytics with configurable chunk sizes up to 50K per chunk.
Game-Wide Analytics
Real-time statistics for your title including player counts, balance totals, and recent activity with SQL optimization.
Circuit Breaker Protection
Built-in protection against database overload with automatic failover and degraded service modes.
Cache Invalidation
INVO clears a playerβs cached balance whenever its own purchase, item purchase, send and transfer paths change it.
Individual Player Lookup
The most common operation - get player balance by email. Optimized for 60M+ player titles with aggressive caching.
π Performance Specs
- β’ Rate Limit: 2,000/minute per title and 100/minute per calling IP
- β’ Cache TTL: 5 minutes for individual lookups
- β’ Response Time: ~50ms (cached), ~200ms (DB)
π Use Cases
- β’ Player login balance display
- β’ Purchase eligibility checks
- β’ Real-time balance updates
- β’ Customer support lookups
// GET /api/player-balances/player/by-email/{email}
const SDK_KEY = 'ivsdk_your_64_character_key_here';
async function getPlayerBalance(playerEmail) {
const response = await fetch(
`https://invo.network/api/player-balances/player/by-email/${encodeURIComponent(playerEmail)}`,
{
method: 'GET',
headers: {
'X-Game-Secret-Key': SDK_KEY,
'Content-Type': 'application/json'
}
}
);
if (response.ok) {
return await response.json();
} else {
throw new Error(`API Error: ${response.status}`);
}
}
// Example usage
try {
const playerData = await getPlayerBalance('player@example.com');
console.log('Player Balance:', playerData);
// Response structure:
// {
// "player": {
// "player_id": 12345,
// "player_name": "PlayerOne",
// "player_email": "player@example.com",
// "date_joined": "2024-01-15T10:30:00Z"
// },
// "balances": [
// {
// "currency_id": 1,
// "currency_name": "Gold Coins",
// "available_balance": "1500.00",
// "reserved_balance": "50.00",
// "total_balance": "1550.00"
// }
// ],
// "summary": {
// "total_value": "1550.00",
// "currency_count": 1,
// "has_funds": true
// },
// "last_updated": "2024-12-09T15:30:45Z"
// }
} catch (error) {
console.error('Failed to get player balance:', error);
}Batch Player Operations
Process up to 100K players per request with memory-efficient chunked processing. Essential for analytics and bulk operations.
// POST /api/player-balances/batch
async function getBatchPlayerBalances(playerEmails = [], playerIds = []) {
const totalRequested = playerEmails.length + playerIds.length;
if (totalRequested > 100000) {
throw new Error('Batch too large. Maximum 100,000 players per request.');
}
const response = await fetch('https://invo.network/api/player-balances/batch', {
method: 'POST',
headers: {
'X-Game-Secret-Key': SDK_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
player_emails: playerEmails,
player_ids: playerIds
})
});
return await response.json();
}
// Example: Batch lookup for analytics
const guildMemberEmails = [
'leader@example.com',
'member1@example.com',
'member2@example.com',
// ... up to 100K emails
];
try {
const batchResult = await getBatchPlayerBalances(guildMemberEmails);
console.log(`Found ${batchResult.summary.total_found} of ${batchResult.summary.total_requested} players`);
// Process results
batchResult.results.forEach(result => {
if (result.error) {
console.log(`Player not found: ${result.player.player_email}`);
} else {
console.log(`${result.player.player_name}: ${result.total_value} total value`);
}
});
// Response structure:
// {
// "results": [
// {
// "player": {
// "player_id": 12345,
// "player_email": "leader@example.com",
// "player_name": "GuildLeader"
// },
// "balances": [...],
// "total_value": "5000.00"
// },
// {
// "player": {"player_email": "notfound@example.com"},
// "error": "Player not found",
// "balances": []
// }
// ],
// "summary": {
// "total_requested": 3,
// "total_found": 2,
// "total_not_found": 1
// },
// "retrieved_at": "2024-12-09T15:30:45Z"
// }
} catch (error) {
console.error('Batch request failed:', error);
}Full Player Data Streaming
Stream ALL players for titles with 60M+ players. Essential for data exports, analytics, and full backups.
β‘ Streaming Features
Performance
- β’ Up to 50K players per chunk
- β’ Memory-efficient processing
- β’ Progress logging every 10K players
- β’ Automatic session cleanup
Controls
- β’ Resume from specific player ID
- β’ Include/exclude zero balances
- β’ Rate limited: 5/hour per title, 2/hour per IP
- β’ One streamed JSON document per call
// GET /api/player-balances/stream-all
async function streamAllPlayers(options = {}) {
const params = new URLSearchParams({
last_player_id: options.lastPlayerId || 0,
chunk_size: Math.min(options.chunkSize || 10000, 50000),
include_zero_balances: options.includeZeroBalances || false
});
const response = await fetch(
`https://invo.network/api/player-balances/stream-all?${params}`,
{
method: 'GET',
headers: {
'X-Game-Secret-Key': SDK_KEY,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`Stream failed: ${response.status}`);
}
// The response is ONE JSON document, streamed: {"status": "streaming", "data": [ ... ],
// "total_streamed": N, "last_player_id": M}. It is not newline-delimited, so read it to the
// end (or use a streaming JSON parser for very large exports) before parsing.
const body = await response.json();
console.log(`Streamed ${body.total_streamed} players; resume after ${body.last_player_id}`);
return body;
}
// Example: Stream with progress tracking
let totalProcessed = 0;
let lastPlayerId = 0;
async function exportAllPlayers() {
const batchSize = 25000; // 25K players per chunk
console.log('Starting full player export...');
try {
await streamAllPlayers({
lastPlayerId: lastPlayerId,
chunkSize: batchSize,
includeZeroBalances: false // Only players with funds
});
console.log(`Export completed. Total processed: ${totalProcessed}`);
} catch (error) {
console.error('Export failed:', error);
console.log(`Resume from player ID: ${lastPlayerId}`);
}
}
// Stream response format:
// {
// "status": "streaming",
// "data": [
// {
// "player_id": 12345,
// "player_email": "player@example.com",
// "player_name": "PlayerName",
// "balances": [
// {
// "currency_id": 1,
// "currency_name": "Gold Coins",
// "available": "1500.00",
// "total": "1550.00"
// }
// ],
// "total_value": "1550.00"
// }
// ],
// "total_streamed": 25000,
// "last_player_id": 67890
// }Game-Wide Analytics
Get essential statistics for your title, optimized for 60M+ players, with SQL-optimized queries and 10-minute caching.
// GET /api/player-balances/game-summary
async function getGameSummary() {
const response = await fetch('https://invo.network/api/player-balances/game-summary', {
method: 'GET',
headers: {
'X-Game-Secret-Key': SDK_KEY,
'Content-Type': 'application/json'
}
});
return await response.json();
}
// Example usage for dashboard
try {
const summary = await getGameSummary();
console.log('Game Analytics:');
console.log(`Total Players: ${summary.players.total_players.toLocaleString()}`);
console.log(`New Players (24h): ${summary.players.new_players_24h}`);
console.log(`Transactions (24h): ${summary.activity.transactions_24h}`);
// Economy breakdown by currency
summary.economy.forEach(currency => {
console.log(`\n${currency.currency_name}:`);
console.log(` Funded Players: ${currency.funded_players.toLocaleString()}`);
console.log(` Total Balance: ${currency.total_balance}`);
console.log(` Average Balance: ${currency.average_balance}`);
});
// Response structure:
// {
// "players": {
// "total_players": 62500000,
// "new_players_24h": 125000
// },
// "economy": [
// {
// "currency_name": "Gold Coins",
// "funded_players": 45000000,
// "total_balance": "2500000000.00",
// "average_balance": "55.56"
// },
// {
// "currency_name": "Premium Gems",
// "funded_players": 15000000,
// "total_balance": "500000000.00",
// "average_balance": "33.33"
// }
// ],
// "activity": {
// "transactions_24h": 850000
// },
// "generated_at": "2024-12-09T15:30:45Z"
// }
} catch (error) {
console.error('Failed to get game summary:', error);
}How fresh a read is
- β’ Balance reads are served from a short cache: up to 5 minutes for a by-email lookup and up to 10 minutes for the game summary. Batch reads and the full stream read the ledger directly.
- β’ INVO clears a player's cached balance itself whenever its purchase, item purchase, send and transfer paths change that player. You do not need to call anything after INVO's own writes.
- β’ For the balance a write just produced, read
new_balanceon that write's response (write-response contract).
Error Handling & Circuit Breakers
Robust error handling with circuit breaker protection to prevent cascade failures in high-volume titles.
π Circuit Breaker
- β’ A temporarily unavailable balance service answers
503 - β’ Retry with backoff; do not hammer it
β οΈ Rate Limiting
- β’ Individual: 2,000/min per title, 100/min per IP
- β’ Batch: 50/min per title, 10/min per IP
- β’ Streaming: 5/hour per title, 2/hour per IP
- β’ Game summary: 100/min per title, 20/min per IP
- β’ Full table: Rate Limits
π₯ Health Monitoring
- β’ Database connectivity
- β’ Cache availability
- β’ Circuit breaker status
- β’ Service degradation alerts
// Comprehensive error handling for player management
class PlayerAPIClient {
constructor(sdkKey) {
this.sdkKey = sdkKey;
this.baseURL = 'https://invo.network/api/player-balances';
}
async request(endpoint, options = {}) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
headers: {
'X-Game-Secret-Key': this.sdkKey,
'Content-Type': 'application/json',
...options.headers
}
});
// Handle specific error cases
switch (response.status) {
case 200:
return await response.json();
case 404:
throw new PlayerNotFoundError('Player not found');
case 429:
const retryAfter = response.headers.get('Retry-After') || 60;
throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
case 503:
const errorData = await response.json();
if (errorData.error === 'service_unavailable') {
throw new CircuitBreakerError('Service temporarily unavailable due to circuit breaker');
}
throw new ServiceError('Service unavailable');
case 401:
throw new AuthenticationError('Invalid SDK key');
case 400:
const badRequestData = await response.json();
throw new ValidationError(badRequestData.message);
default:
throw new APIError(`API request failed with status ${response.status}`);
}
}
async getPlayerBalanceWithRetry(playerEmail, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await this.request(`/player/by-email/${encodeURIComponent(playerEmail)}`);
} catch (error) {
if (error instanceof CircuitBreakerError && attempt < maxRetries) {
// Wait before retry for circuit breaker
await new Promise(resolve => setTimeout(resolve, 30000));
continue;
}
if (error instanceof RateLimitError && attempt < maxRetries) {
// Exponential backoff for rate limits
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error; // Re-throw if max retries exceeded
}
}
}
}
// Custom error classes
class PlayerNotFoundError extends Error {}
class RateLimitError extends Error {}
class CircuitBreakerError extends Error {}
class AuthenticationError extends Error {}
class ValidationError extends Error {}
class ServiceError extends Error {}
class APIError extends Error {}
// Usage with error handling
const client = new PlayerAPIClient('ivsdk_your_key_here');
try {
const playerData = await client.getPlayerBalanceWithRetry('player@example.com');
console.log('Player data retrieved successfully:', playerData);
} catch (error) {
if (error instanceof PlayerNotFoundError) {
console.log('Player not found - might be a new user');
} else if (error instanceof RateLimitError) {
console.log('Rate limited - implementing backoff');
} else if (error instanceof CircuitBreakerError) {
console.log('Service degraded - using cached data or fallback');
} else {
console.error('Unexpected error:', error.message);
}
}Performance Optimization Tips
Best practices for optimal performance when managing millions of players.
β‘ Query Optimization
- β’ Cache First: Always check cache before API calls
- β’ Batch Wisely: Use batch endpoint for 100+ players
- β’ Stream Large Exports: Use streaming for full data exports
π― Integration Patterns
- β’ Server-Side Only: Never call from client code
- β’ Connection Pooling: Reuse HTTP connections
- β’ Async Processing: Use Promise.all for parallel requests
- β’ Circuit Breaker Aware: Handle degraded service gracefully
Service Health Monitoring
Monitor API health and performance with built-in health check endpoints.
Rate Limits & Usage Guidelines
Essential limits to ensure optimal performance for all titles on the network:
Limits (per title; per calling IP)
- β’ Individual Lookups: 2,000/minute; 100/minute
- β’ Batch Requests: 50/minute; 10/minute
- β’ Game Summary: 100/minute; 20/minute
- β’ Full Streaming: 5/hour; 2/hour
Best Practices
- β’ For the result of a write, use its
new_balanceinstead of reading again - β’ Use batch endpoint for 100+ concurrent lookups
- β’ Implement exponential backoff for rate limit errors
- β’ Monitor circuit breaker state for service health
Real-World Integration Examples
Common integration patterns for different scenarios and player volumes.
π° Guild Management System
// Guild balance checking for raid eligibility
class GuildManager {
constructor(sdkKey) {
this.apiClient = new PlayerAPIClient(sdkKey);
}
async checkRaidEligibility(guildMembers, minimumBalance = 1000) {
// Use batch API for guild member lookup
const memberEmails = guildMembers.map(member => member.email);
try {
const batchResult = await this.apiClient.request('/batch', {
method: 'POST',
body: JSON.stringify({ player_emails: memberEmails })
});
const eligibleMembers = [];
const ineligibleMembers = [];
batchResult.results.forEach(result => {
if (result.error) {
ineligibleMembers.push({
email: result.player.player_email,
reason: 'Player not found'
});
return;
}
const totalValue = parseFloat(result.total_value);
if (totalValue >= minimumBalance) {
eligibleMembers.push({
...result.player,
totalValue
});
} else {
ineligibleMembers.push({
...result.player,
totalValue,
reason: `Insufficient funds (needs ${minimumBalance}, has ${totalValue})`
});
}
});
return {
eligible: eligibleMembers,
ineligible: ineligibleMembers,
eligibilityRate: (eligibleMembers.length / memberEmails.length) * 100
};
} catch (error) {
console.error('Guild eligibility check failed:', error);
throw error;
}
}
}π Leaderboard Generation
// Generate wealth leaderboards from streaming data
class LeaderboardGenerator {
constructor(sdkKey) {
this.apiClient = new PlayerAPIClient(sdkKey);
}
async generateWealthLeaderboard(topN = 100) {
const leaderboard = [];
let processedCount = 0;
console.log('Starting wealth leaderboard generation...');
try {
// Stream all players and track top performers
const response = await fetch('https://invo.network/api/player-balances/stream-all?include_zero_balances=false', {
headers: {
'X-Game-Secret-Key': this.apiClient.sdkKey
}
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const players = this.parseStreamChunk(chunk);
players.forEach(player => {
const totalValue = parseFloat(player.total_value);
if (totalValue > 0) {
leaderboard.push({
playerId: player.player_id,
playerName: player.player_name,
totalValue: totalValue
});
// Keep only top N + buffer for efficiency
if (leaderboard.length > topN * 2) {
leaderboard.sort((a, b) => b.totalValue - a.totalValue);
leaderboard.splice(topN);
}
}
processedCount++;
if (processedCount % 50000 === 0) {
console.log(`Processed ${processedCount.toLocaleString()} players...`);
}
});
}
// Final sort and trim
leaderboard.sort((a, b) => b.totalValue - a.totalValue);
const finalLeaderboard = leaderboard.slice(0, topN).map((player, index) => ({
rank: index + 1,
...player
}));
console.log(`Leaderboard generated from ${processedCount.toLocaleString()} players`);
return finalLeaderboard;
} catch (error) {
console.error('Leaderboard generation failed:', error);
throw error;
}
}
parseStreamChunk(chunk) {
// Parse streaming JSON data (simplified)
try {
return JSON.parse(chunk).data || [];
} catch {
return [];
}
}
}π§ Customer Support Tools
// Customer support player lookup with comprehensive info
class CustomerSupportAPI {
constructor(sdkKey) {
this.apiClient = new PlayerAPIClient(sdkKey);
}
async getPlayerSupportInfo(playerEmail) {
try {
// Get current balance
const balanceData = await this.apiClient.request(
`/player/by-email/${encodeURIComponent(playerEmail)}`
);
if (!balanceData.player) {
return {
found: false,
message: 'Player not found in system'
};
}
// Get game-wide context for comparison
const gameSummary = await this.apiClient.request('/game-summary');
// Calculate player percentiles
const playerTotalValue = parseFloat(balanceData.summary.total_value);
const avgBalance = parseFloat(gameSummary.economy[0]?.average_balance || 0);
return {
found: true,
player: {
...balanceData.player,
accountAge: this.calculateAccountAge(balanceData.player.date_joined),
balances: balanceData.balances,
totalValue: playerTotalValue,
comparedToAverage: playerTotalValue / avgBalance,
hasActiveBalances: balanceData.summary.has_funds,
currencyCount: balanceData.summary.currency_count
},
gameContext: {
totalPlayers: gameSummary.players.total_players,
averageBalance: avgBalance,
newPlayersToday: gameSummary.players.new_players_24h
},
supportNotes: this.generateSupportNotes(balanceData, gameSummary)
};
} catch (error) {
if (error instanceof PlayerNotFoundError) {
return {
found: false,
message: 'Player not found'
};
}
throw error;
}
}
calculateAccountAge(dateJoined) {
if (!dateJoined) return 'Unknown';
const joinDate = new Date(dateJoined);
const now = new Date();
const diffMs = now - joinDate;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays < 1) return 'Less than 1 day';
if (diffDays < 30) return `${diffDays} days`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months`;
return `${Math.floor(diffDays / 365)} years`;
}
generateSupportNotes(balanceData, gameSummary) {
const notes = [];
const totalValue = parseFloat(balanceData.summary.total_value);
const avgBalance = parseFloat(gameSummary.economy[0]?.average_balance || 0);
if (totalValue === 0) {
notes.push('β οΈ Player has zero balance across all currencies');
} else if (totalValue > avgBalance * 10) {
notes.push('π High-value player (10x+ average balance)');
} else if (totalValue < avgBalance * 0.1) {
notes.push('π Below-average balance (less than 10% of average)');
}
if (balanceData.summary.currency_count > 1) {
notes.push(`πͺ Uses ${balanceData.summary.currency_count} different currencies`);
}
return notes;
}
}Security & Best Practices
Essential security practices for managing player data at scale:
π Authentication & Security
- β’ Always use SDK key in
X-Game-Secret-Keyheader on every request - β’ Never expose SDK keys in client-side code or logs
- β’ Store SDK keys in secure environment variables
- β’ Use HTTPS for all API communications
- β’ Implement proper error handling to prevent data leaks
β‘ Performance & Reliability
- β’ Implement client-side caching with appropriate TTLs
- β’ Use batch endpoints for multiple player lookups
- β’ Handle rate limits with exponential backoff
- β’ Monitor circuit breaker status for service health