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

2

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.

3

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

POST

/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_ids field
  • • 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

0

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.

1

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.

2

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.

3

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%)

Source tenant fee3.5%
Target tenant fee3.5%
Network fee (Invo)3.0%
Total user-paid fee10.0%

Example transfer

Transfer amount:1,000 Gold
Total fees (10%):-100 Gold

Recipient receives:900 Gold

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%

Source
5.0%
Target
3.5%
Invo
1.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_phone required 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.