Cross-Game Transfers Overview
Enable secure, verified transfers of in-game currency between different games in your ecosystem. Our transfer system uses SMS verification and claim codes to ensure only authorized transfers are completed.
Integration Flow for Game Developers
Step 1: Get Available Destinations (Cache Daily)
Before building your transfer UI, fetch the list of games your players can transfer to. This data should be cached locally and refreshed once per day.
POST /api/transfers/available-destinations
• Cache Duration: 24 hours (games don't change frequently)
• Refresh Trigger: Daily app startup or manual refresh
• Storage: Local file, localStorage, or in-memory cache
• Performance: Prevents API calls every time player opens transfer UI
Build Transfer UI
Use the cached destination data to create your transfer interface. Show game names, icons, currency types, and transfer limits.
UI Elements: Game selection dropdown, currency display, amount input with min/max validation, fee preview.
Process Transfer
When player confirms transfer, call the initiate → verify → claim sequence using the existing transfer APIs.
API Sequence: /initiate-transfer → /verify-sms → /claim-transfer (recipient's game)
Note: handle HTTP 202. If the initiating account is flagged as a minor on Invo, /initiate-transfer returns 202 pending_guardian_approval with a polling endpoint. Adult accounts are unaffected. See Guardian Approval.
Available Destinations API
/api/transfers/available-destinations
Get games that players can transfer currency to
Request Body
{
"source_game_id": "123456789012"
}Headers
X-Game-Secret-Key: your_secret_key Content-Type: application/json
Sample Response
{
"status": "success",
"source_game_name": "Adventure Quest",
"universal_transfers": "yes",
"transfer_mode": "universal",
"available_games": [
{
"game_id": "987654321098",
"game_name": "Space Warriors",
"developer_name": "Stellar Studios",
"genre": "Action",
"game_icon": "https://cdn.example.com/space_icon.png",
"currency_name": "Energy Credits",
"currency_symbol": "EC",
"minimum_transfer": "5.00",
"maximum_transfer": "1000.00"
},
{
"game_id": "111222333444",
"game_name": "Fantasy Kingdom",
"developer_name": "Magic Games",
"genre": "RPG",
"game_icon": "https://cdn.example.com/fantasy_icon.png",
"currency_name": "Gold Coins",
"currency_symbol": "GC",
"minimum_transfer": "1.00",
"maximum_transfer": null
}
],
"total_destinations": 2
}Transfer Mode Logic
Universal Transfers
When your game has universal_transfers = "yes"
- • Players can transfer to ALL live games
- • No configuration needed
- • Maximum ecosystem connectivity
- • Excludes testing/inactive games
Linked Transfers
When your game has universal_transfers = "no"
- • Players can transfer ONLY to linked games
- • Uses your game's
linked_game_idsfield - • Controlled partnerships
- • Curated transfer destinations
Caching Implementation Guide
Why Cache This API?
- • Performance: Instant transfer UI loading
- • Reliability: Works even if API is temporarily unavailable
- • Cost: Reduces API calls (game list doesn't change often)
- • User Experience: No loading delays when opening transfer screen
UnityUnity C# Caching Implementation
// Cache destination games data
public class TransferDestinationsCache : MonoBehaviour
{
private const string CACHE_KEY = "transfer_destinations";
private const string CACHE_TIME_KEY = "destinations_cached_at";
private const int CACHE_DURATION_HOURS = 24;
[System.Serializable]
public class DestinationsData
{
public string status;
public string source_game_name;
public string universal_transfers;
public GameDestination[] available_games;
public int total_destinations;
}
public void CacheDestinations(DestinationsData data)
{
string json = JsonUtility.ToJson(data);
PlayerPrefs.SetString(CACHE_KEY, json);
PlayerPrefs.SetString(CACHE_TIME_KEY, System.DateTime.Now.ToBinary().ToString());
PlayerPrefs.Save();
Debug.Log(quot;Cached {data.total_destinations} transfer destinations");
}
public DestinationsData GetCachedDestinations()
{
if (!PlayerPrefs.HasKey(CACHE_KEY)) return null;
// Check if cache is expired
if (IsCacheExpired())
{
Debug.Log("Destinations cache expired, refreshing...");
RefreshDestinations();
return null;
}
string json = PlayerPrefs.GetString(CACHE_KEY);
return JsonUtility.FromJson<DestinationsData>(json);
}
private bool IsCacheExpired()
{
if (!PlayerPrefs.HasKey(CACHE_TIME_KEY)) return true;
long cacheTicks = System.Convert.ToInt64(PlayerPrefs.GetString(CACHE_TIME_KEY));
System.DateTime cacheTime = System.DateTime.FromBinary(cacheTicks);
return (System.DateTime.Now - cacheTime).TotalHours > CACHE_DURATION_HOURS;
}
public void RefreshDestinations()
{
StartCoroutine(FetchDestinationsFromAPI());
}
private IEnumerator FetchDestinationsFromAPI()
{
// Call the available-destinations API here
// Cache the result using CacheDestinations()
}
}WebJavaScript/Web Caching Implementation
class TransferDestinationsCache {
constructor() {
this.CACHE_KEY = 'invo_transfer_destinations';
this.CACHE_TIME_KEY = 'invo_destinations_cached_at';
this.CACHE_DURATION_HOURS = 24;
}
async getDestinations(gameId, secretKey) {
// Try to get from cache first
const cached = this.getCachedDestinations();
if (cached) {
console.log('Using cached transfer destinations');
return cached;
}
// Cache miss or expired, fetch from API
console.log('Fetching fresh transfer destinations...');
return await this.fetchAndCacheDestinations(gameId, secretKey);
}
getCachedDestinations() {
const cachedData = localStorage.getItem(this.CACHE_KEY);
const cacheTime = localStorage.getItem(this.CACHE_TIME_KEY);
if (!cachedData || !cacheTime) return null;
// Check if expired
const now = new Date().getTime();
const cached = new Date(parseInt(cacheTime)).getTime();
const hoursOld = (now - cached) / (1000 * 60 * 60);
if (hoursOld > this.CACHE_DURATION_HOURS) {
console.log('Cache expired, need fresh data');
return null;
}
return JSON.parse(cachedData);
}
async fetchAndCacheDestinations(gameId, secretKey) {
try {
const response = await fetch('/api/transfers/available-destinations', {
method: 'POST',
headers: {
'X-Game-Secret-Key': secretKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ source_game_id: gameId })
});
const data = await response.json();
if (data.status === 'success') {
// Cache the successful response
localStorage.setItem(this.CACHE_KEY, JSON.stringify(data));
localStorage.setItem(this.CACHE_TIME_KEY, new Date().getTime().toString());
console.log(`Cached ${data.total_destinations} destinations`);
}
return data;
} catch (error) {
console.error('Failed to fetch destinations:', error);
throw error;
}
}
clearCache() {
localStorage.removeItem(this.CACHE_KEY);
localStorage.removeItem(this.CACHE_TIME_KEY);
}
}
// Usage example
const cache = new TransferDestinationsCache();
const destinations = await cache.getDestinations('123456789012', 'your_secret_key');Transfer Process
4 Steps
Get Destinations → Initiate → Verify → Claim
Security
SMS + PIN
Two-factor verification
Transfer Fee
10%
Platform + game fees
Cache Duration
24 Hours
Destinations API cache
How Cross-Game Transfers Work
Get Available Destinations (Daily)
Before showing transfer options, your game calls the destinations API to get the current list of games players can transfer to. This data is cached locally for 24 hours for performance.
What happens: API returns game list based on universal_transfers setting, data cached locally, transfer UI populated.
Initiate Transfer
Player selects destination game from cached list, enters target recipient phone, amount, and confirms transfer. The system validates the transfer, calculates fees, and reserves the amount from their balance. 🔒 The claim code is locked to the target phone.
What happens: Amount is reserved, claim code locked to target phone, SMS PIN generated and sent, transfer enters "pending verification" status.
SMS Verification
Player receives an SMS with a verification PIN (valid for 10 minutes). They enter this PIN in the source game to confirm they authorized the transfer. This prevents unauthorized transfers.
What happens: PIN is verified, claim code is generated and sent via SMS, transfer enters "pending claim" status.
Claim Transfer
Recipient uses the claim code in the target game to receive the transferred currency. 🔒 The claim code can ONLY be redeemed by the exact phone number specified during initiation. Valid for 24 hours, one-time use only.
What happens: Phone number verified, currency credited to recipient's balance, transfer completed, confirmation SMS sent.
Fee Structure
Default split (both tenants at 3.5%)
Example transfer
Per-tenant rate overrides
Each tenant has a transfer_fee_percentage field (default 3.5%, range 0–10) that controls its own kickback share on every transfer/send it participates in. Negotiated rates above the default come out of Invo's 3% slice — not the other side's. The user-paid total stays at exactly 10%.
Example: source tenant overridden to 5%, target at default 3.5%
Invo's slice absorbs the +1.5% above source's default. Both override sides combined cannot exceed 10% — at runtime, side fees scale down proportionally if they would otherwise push Invo below 0.
Always read fee_breakdown from the API response and fee_total from the webhook payload — those reflect the actual numbers applied to that specific transfer. Do not recompute fees client-side.
Rate Limits & Restrictions
Current Limits (Launch Period)
Destinations API
- • 100 requests per minute per IP
- • Should be cached for 24 hours
- • Call once daily at app startup
Per Player Limits
- • 100 transfer initiations per hour
- • 40 SMS verifications per hour
- • 30 claim attempts per hour
- • 10 transfers per hour (velocity limit)
- • 5,000 currency units per day
Per IP Address Limits
- • 300 initiations per hour
- • 200 verifications per hour
- • 150 claims per hour
- • 20 requests per minute
Note: These limits are generous during the initial launch period and may be adjusted based on usage patterns and security requirements.
Integration Best Practices
Caching Strategy
- • Call /available-destinations once per day, cache locally
- • Use cached data to build transfer UI instantly
- • Refresh cache on app startup or manual refresh
- • Store in persistent storage (not just memory)
- • Handle cache miss gracefully with loading states
User Experience
- • Show game icons and names from destinations data
- • Display currency symbols and names clearly
- • Validate transfer amounts against min/max limits
- • Preview fees before confirming transfer
- • Provide clear error messages for validation failures
Performance
- • Never call destinations API when player opens transfer UI
- • Use background refresh to update cache silently
- • Implement proper error handling for API failures
- • Show fallback UI if destinations data unavailable
- • Consider CDN for game icons/assets
Error Handling
- • Handle "no destinations available" gracefully
- • Show appropriate messages for universal vs linked modes
- • Retry failed destination requests with backoff
- • Log destination API failures for debugging
- • Provide manual refresh option for users
Security & Anti-Fraud Features
🔒 Phone-Based Claim Security
Transfer claim codes are now locked to specific phone numbers. Only the target phone specified during initiation can redeem the claim code.
- •
target_player_phonerequired at initiation - • Claim codes can ONLY be redeemed by the specified phone number
- • Phone mismatch results in 403 Forbidden error
- • Prevents unauthorized claim code sharing and redemption
SMS Verification
- • SMS PIN required to authorize transfers
- • 10-minute expiration window
- • Maximum 3 attempts per PIN
- • Rate limiting on SMS requests
- • Phone-locked claim codes
Fraud Prevention
- • Transfer velocity limits per player
- • Duplicate transfer detection
- • IP-based rate limiting
- • Failed attempt tracking and blocking
Time Limits
- • SMS PIN: 10 minutes to verify
- • Claim code: 24 hours to claim
- • Expired transfers return funds automatically
- • No funds lost due to expiration
Player Protection
- • Funds reserved during verification
- • Automatic refunds on failure/expiration
- • Clear status tracking throughout process
- • SMS notifications at each step
Transfer Status Definitions
pending_pin_verification
Transfer initiated, waiting for SMS PIN verification. Funds are reserved.
pending_claim
SMS verified, claim code sent. Waiting for recipient to claim the transfer.
completed
Transfer successfully claimed and completed. Currency credited to recipient.
failed / expired
Transfer failed verification, expired, or was cancelled. Funds returned to sender.