Initiate Player Currency Send

Start a secure currency send to another player via their phone number. This endpoint supports both peer-to-peer sends within your game and cross-game currency sends to other games in the Invo ecosystem.

Currency Send Types

Peer-to-Peer Sends

Send currency to another player within your own game by setting receiving_game_id to your game's ID.

  • • Player-to-player transactions
  • • Marketplace purchases
  • • Gifting between friends
  • • In-game services and tips

Cross-Game Sends

Send currency to players in other games by setting receiving_game_id to a different game's ID.

  • • Cross-game community building
  • • Ecosystem-wide interactions
  • • Portfolio value distribution
  • • Multi-game rewards
POST

/api/currency-sends/initiate-send

Initiates a player-to-player currency send with SMS verification

Security Requirement

This endpoint must only be called from your secure game server. Never call it directly from a game client.

Authentication Required

All requests must include your game's secret key in the request headers:

X-Game-Secret-Key: your_game_secret_key_here

Policy Requirements

For cross-game sends, your game's transfer policy determines which destinations are allowed:

  • Universal Transfers: Can send to any live game in the ecosystem
  • Linked Transfers: Can only send to games in your linked_game_ids list
  • Testing Games: Cannot send to or from games in testing status

Request Body Parameters

ParameterTypeRequiredDescription
client_request_idstringYesUnique ID for this send attempt (prevents duplicates)
sender_player_namestringYesDisplay name of the player sending the currency
sender_player_emailstringYesEmail address of the sending player
sender_player_phonestringYesPhone number for SMS verification (format: +1234567890)
receiver_player_emailstringYes🔒 SECURITY: Receiver's email address. Required as of 2026-05-07 so the phone-share approval gate can fire at initiate time, before any transaction is staged. If this email is new on the network and the supplied phone is already on file under a different email, the response is a 409 PHONE_SHARE_APPROVAL_REQUIRED with an SMS auto-sent to the existing phone owner.
receiver_player_phonestringYes🔒 SECURITY: Receiver's phone number. Claim code can ONLY be redeemed by this number (format: +1234567890)
receiving_game_idintegerYesID of the game where currency will be received (same ID for peer-to-peer)
amountstringYesAmount to send (as decimal string, e.g. "100.50")

Implementation Examples

UnityUnity C# Currency Send Initiation

// This function MUST be called on your secure server, NOT the client.
public IEnumerator InitiateCurrencySend(string clientRequestId, string senderName,
    string senderEmail, string senderPhone, string receiverEmail, string receiverPhone,
    int receivingGameId, string amount, Action<string> onSuccess, Action<string> onError)
{
    string url = "https://invo.network/api/currency-sends/initiate-send";

    var requestData = new {
        client_request_id = clientRequestId,
        sender_player_name = senderName,
        sender_player_email = senderEmail,
        sender_player_phone = senderPhone,
        receiver_player_email = receiverEmail, // REQUIRED 2026-05-07: gates phone-share check at initiate
        receiver_player_phone = receiverPhone,
        receiving_game_id = receivingGameId, // Same as your game ID for peer-to-peer
        amount = amount
    };
    string jsonBody = JsonUtility.ToJson(requestData);

    using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
    {
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        
        request.SetRequestHeader("X-Game-Secret-Key", "your_game_secret_key_here");
        request.SetRequestHeader("Content-Type", "application/json");

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            onSuccess?.Invoke(request.downloadHandler.text);
        }
        else
        {
            onError?.Invoke(request.error + ": " + request.downloadHandler.text);
        }
    }
}

UnrealUnreal C++ Currency Send Initiation

// This function MUST be called on your secure server.
void AYourGameMode::InitiateCurrencySend(const FString& ClientRequestId,
    const FString& SenderName, const FString& SenderEmail,
    const FString& SenderPhone, const FString& ReceiverEmail,
    const FString& ReceiverPhone, int32 ReceivingGameId, const FString& Amount)
{
    TSharedPtr<FJsonObject> RequestObj = MakeShareable(new FJsonObject);
    RequestObj->SetStringField("client_request_id", ClientRequestId);
    RequestObj->SetStringField("sender_player_name", SenderName);
    RequestObj->SetStringField("sender_player_email", SenderEmail);
    RequestObj->SetStringField("sender_player_phone", SenderPhone);
    RequestObj->SetStringField("receiver_player_email", ReceiverEmail); // REQUIRED 2026-05-07: gates phone-share check at initiate
    RequestObj->SetStringField("receiver_player_phone", ReceiverPhone);
    RequestObj->SetNumberField("receiving_game_id", ReceivingGameId);
    RequestObj->SetStringField("amount", Amount);

    FString RequestBody;
    TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
    FJsonSerializer::Serialize(RequestObj.ToSharedRef(), Writer);

    TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
    Request->SetURL("https://invo.network/api/currency-sends/initiate-send");
    Request->SetVerb("POST");
    Request->SetHeader("X-Game-Secret-Key", "your_game_secret_key_here");
    Request->SetHeader("Content-Type", "application/json");
    Request->SetContentAsString(RequestBody);
    
    Request->OnProcessRequestComplete().BindUObject(this, &AYourGameMode::OnSendInitiated);
    Request->ProcessRequest();
}

GodotGodot GDScript Currency Send Initiation

# This function MUST be called on your secure server.
func initiate_currency_send(client_request_id: String, sender_name: String,
    sender_email: String, sender_phone: String,
    receiver_email: String, receiver_phone: String,
    receiving_game_id: int, amount: String):

    var url = "https://invo.network/api/currency-sends/initiate-send"
    var headers = [
        "X-Game-Secret-Key: your_game_secret_key_here",
        "Content-Type: application/json"
    ]
    var body = {
        "client_request_id": client_request_id,
        "sender_player_name": sender_name,
        "sender_player_email": sender_email,
        "sender_player_phone": sender_phone,
        "receiver_player_email": receiver_email, # REQUIRED 2026-05-07: gates phone-share check at initiate
        "receiver_player_phone": receiver_phone,
        "receiving_game_id": receiving_game_id, # Same as your game ID for peer-to-peer
        "amount": amount
    }
    
    http_request.request(url, headers, HTTPClient.METHOD_POST, JSON.stringify(body))

Request & Response Examples

Example: Peer-to-Peer Send (Same Game)

POST /api/currency-sends/initiate-send
Content-Type: application/json
X-Game-Secret-Key: your_game_secret_key_here

{
  "client_request_id": "p2p_send_12345_20240624",
  "sender_player_name": "PlayerOne",
  "sender_player_email": "player1@example.com",
  "sender_player_phone": "+1234567890",
  "receiver_player_email": "recipient@example.com",
  "receiver_player_phone": "+1987654321",
  "receiving_game_id": 123456789012,
  "amount": "500.00"
}

// Note: receiving_game_id matches your own game ID for peer-to-peer sends
// Note: receiver_player_email is REQUIRED as of 2026-05-07 — required so the
//       phone-share approval gate can fire at initiate time, before any
//       transaction is staged. See PHONE_SHARE_APPROVAL_REQUIRED docs.

Example: Cross-Game Send

POST /api/currency-sends/initiate-send
Content-Type: application/json
X-Game-Secret-Key: your_game_secret_key_here

{
  "client_request_id": "cross_send_67890_20240624",
  "sender_player_name": "PlayerOne",
  "sender_player_email": "player1@example.com",
  "sender_player_phone": "+1234567890",
  "receiver_player_email": "recipient@example.com",
  "receiver_player_phone": "+1987654321",
  "receiving_game_id": 987654321098,
  "amount": "250.00"
}

// Note: receiving_game_id is different from your game ID for cross-game sends

Watch for HTTP 202 — guardian approval pending

If the sending account is flagged as a minor on Invo, this endpoint returns 202 Accepted instead of 201, with a guardian_approval block. The send is held until the guardian replies YES or NO via SMS within 15 minutes. This is the standard integration pattern going forward — handle it defensively on every initiate call; you can't detect minor status from the partner side.

Full contract: Guardian Approval.

Success Response (201 Created)

{
  "status": "success",
  "message": "Currency send initiated. Please verify with the SMS PIN sent to your phone.",
  "transaction_id": "txn_send123abc456",
  "order_id": "ord_send789def012",
  "send_details": {
    "sending_game": "Adventure Quest",
    "receiving_game": "Space Warriors",
    "receiving_game_id": "987654321098",
    "currency": "Gold",
    "currency_id": 1,
    "amount_initiated": "500.00",
    "fees_preview": {
      "total_fee": "50.00",
      "sending_game_fee": "17.50",
      "receiving_game_fee": "17.50",
      "platform_fee": "15.00",
      "net_amount": "450.00"
    },
    "receiver_phone": "+1987654321",
    "send_type": "cross_game",
    "transfer_policy": {
      "source_universal_transfers": "yes",
      "policy_applied": "universal",
      "target_in_linked_list": null
    }
  },
  "verification_required": {
    "phone_number_masked": "******7890",
    "pin_expires_in_minutes": 10
  }
}

Pending Guardian Approval (202 Accepted)

{
  "status": "pending_guardian_approval",
  "message": "Currency send initiated. SMS PIN sent to your phone, but verification is held until the guardian on file approves via SMS.",
  "transaction_id": "txn_send123abc456",
  "order_id": "ord_send789def012",
  "guardian_approval": {
    "approval_id": "1f2a8c0d-...",
    "state": "pending",
    "expires_at": "2026-05-04T22:15:00+00:00",
    "poll_endpoint": "/api/transactions/txn_send123abc456/approval-status"
  },
  "send_details": { /* same as 201 success */ },
  "verification_required": { /* same as 201 success */ }
}

Show a "waiting for parent approval" UI and poll poll_endpoint every 5–10s. See Guardian Approval for the full state machine.

Error Response - Phone-Share Approval Required (409 Conflict)

{
  "status": "error",
  "error_code": "PHONE_SHARE_APPROVAL_REQUIRED",
  "message": "This phone number is already linked to another Invo account. The existing phone owner must approve via SMS before this email can also use this phone.",
  "phone": "+15551234567",
  "requesting_email": "bob@example.com",
  "existing_account_hints": ["al***@example.com"],
  "next_endpoint": "/api/wallet/phone-share/initiate",
  "approval_id": "1f2a8c0d-...",
  "expires_at": "2026-05-04T22:25:00+00:00",
  "code_sent": true,
  "already_approved": false
}

Fires when the supplied phone is already on file with a different email. Same contract for game-developer SDK integrations and platform-tier partners. The OTP SMS is auto-sent — have the existing phone owner read the SMS, then call POST /api/wallet/phone-share/approve with the approval_id from the body and the 6-digit OTP, then retry the original call. Only call /phone-share/initiate manually if code_sent: false. Match on error_code, not message text.

Initiate-time gate (since 2026-05-07): the gate now fires at /initiate-send as well as /claim-currency. Previously the conflict was only detected at claim time, after the sender had reserved funds and received an SMS-PIN. Now the 409 fires at initiate, before any Transaction, TransferPin, or OrderBook row is created. Net effect: zero side effects until the existing phone owner has approved.

If already_approved: true: auto-retry the original action immediately, do not show the OTP entry UI. The flag means a concurrent approval landed between gate-check and response — the pair is approved on the backend; the gate fired in a brief race window. The retry will succeed. One retry, no loops.

Anti-abuse fields (optional, present only when triggered): cooldown_seconds appears when a fresh OTP for the same (phone, email) was minted less than 30s ago — no new SMS is sent, the previous code on the user's phone is still valid, and the included approval_id is the same row. Show the OTP entry panel as normal; do not show "code not sent" copy and do not auto-call /phone-share/initiate until the cooldown elapses. rate_limited: true + retry_after_seconds appear when this phone has hit the platform-wide 40-OTP-per-hour cap — no SMS sent, no approval_id returned, surface "please wait before requesting another code."

Three valid response paths the existing phone owner can choose between:

  • Approve via UI: type the 6-digit code into your UI → call POST /api/wallet/phone-share/approve with { approval_id, otp }.
  • Approve via SMS reply: text the code (or YES <code>) back from their phone. Invo's inbound webhook stamps the approval server-side.
  • Reject (new, 2026-05-14): either call POST /api/wallet/phone-share/reject with { approval_id, otp } from your UI, OR reply NO (or NO <code>) by SMS from the phone on file. Rejected rows are terminal — the requesting email must re-trigger a fresh request to retry. A subsequent /phone-share/initiate for the same pair creates a new approval row.

If a user takes either SMS path, your client gets no direct callback — poll GET /api/wallet/phone-share/status?phone=...&email=... every 3–5s while your panel is open. Auto-retry the original action when approved: true; surface a clear rejection message and offer a "try again" affordance if the user reports a NO reply (the status endpoint returns approved: false for both pending and rejected states — distinguishable only by the existence of a newer pending row after a fresh initiate).

Error Response - Policy Violation (403 Forbidden)

{
  "status": "error",
  "message": "Currency send not allowed: Receiving game 'Space Warriors' (ID: 987654321098) is not in the linked games list for sending game 'Adventure Quest'. Linked games: [123456789012, 456789012345]",
  "error_code": "CURRENCY_SEND_POLICY_VIOLATION"
}

Error Response - Testing Game (403 Forbidden)

{
  "status": "error",
  "message": "Currency sends are not allowed to games in testing status. Receiving game 'Test Game' must be live to receive currency sends."
}

Error Response - Rate Limited (429 Too Many Requests)

{
  "error": "velocity_limit_exceeded",
  "message": "Hourly currency send limit of 25 sends exceeded. Please try again later."
}

What Happens After Initiation

SMS PIN Sent to Sender

The sending player receives an SMS with a 6-digit verification PIN at their phone number. The message format is:

"Your Invo currency send from Adventure Quest to Space Warriors verification code is 123456. Valid for 10 minutes. Our employees will never ask you for this code."

Funds Reserved

The send amount is immediately reserved from the sender's available balance. This ensures the funds are secured for the send but not yet deducted. If the send fails or expires, the reserved amount is automatically returned to their available balance.

Next Steps

The sender has 10 minutes to verify the SMS PIN using the /verify-sms endpoint. After verification, a claim code will be automatically sent to the receiver's phone number.

Validation Rules & Restrictions

Phone Number Format

  • • Both sender and receiver phones must start with country code (e.g., +1 for US)
  • • Must be 10-15 digits total after country code
  • • Only digits allowed after the + symbol
  • • Examples: +1234567890, +441234567890

Send Amount Limits

  • • Must be greater than currency minimum (usually 0.01)
  • • Must not exceed currency maximum (if set by game)
  • • Must not exceed sender's available balance
  • • Daily send limit: 10,000 currency units per player
  • • Hourly send limit: 25 sends per player

Game Status Requirements

  • Sending Game: Must be in 'live' status (not 'testing')
  • Receiving Game: Must be in 'live' status (not 'testing')
  • Peer-to-Peer: Always allowed within the same live game
  • Cross-Game: Requires both games to be live and policy-compliant

Transfer Policy Requirements

  • Universal Transfers (universal_transfers: "yes"): Can send to any live game
  • Linked Transfers (universal_transfers: "no"): Can only send to games in linked_game_ids
  • • Sending game must have allows_outgoing_transfers enabled
  • • Target game must be found in ecosystem

Duplicate Prevention

  • • client_request_id must be unique per game
  • • Rapid identical sends (same amount/receiver) are blocked
  • • Rate limiting prevents spam attempts
  • • Velocity limits prevent excessive send activity

Common Error Scenarios

HTTP 400 - Bad Request

  • • Missing required fields
  • • Invalid phone number format (sender or receiver)
  • • Invalid amount (negative, too large, etc.)
  • • Receiving game not found
  • • Insufficient balance

HTTP 403 - Forbidden

  • • Game in testing status (sender or receiver)
  • • Transfer policy violation (not in linked games)
  • • Outgoing transfers not allowed
  • • Universal transfers disabled and target not linked

HTTP 409 - Conflict

  • • Duplicate client_request_id
  • • Send already initiated or processed

HTTP 429 - Too Many Requests

  • • Player exceeded hourly send initiation limit (25/hour)
  • • IP address exceeded rate limits
  • • Velocity limits exceeded (too many sends)
  • • Rapid identical send detection

HTTP 500 - Server Error

  • • SMS service unavailable
  • • Database connectivity issues
  • • Internal system errors

Implementation Best Practices

User Experience

  • • Use the /available-destinations endpoint to get valid receiving games
  • • Validate phone numbers before submitting
  • • Show send fee preview before confirmation
  • • Display clear error messages for validation failures
  • • Provide phone number formatting hints
  • • Show receiving game name clearly in send UI
  • • Distinguish between peer-to-peer and cross-game sends in UI

Error Handling

  • • Parse API error responses for user-friendly messages
  • • Handle rate limiting with appropriate retry delays
  • • Implement exponential backoff for server errors
  • • Log all send attempts for debugging and monitoring
  • • Provide fallback options when services are unavailable
  • • Handle policy violations gracefully with clear explanations

Security

  • • Always call from secure server, never from game client
  • • Validate all input data before API submission
  • • Use unique client_request_id for each send
  • • Implement proper timeout handling
  • • Never expose game secret keys in client code
  • • Validate receiving_game_id against allowed destinations

Send Flow Management

  • • Save transaction_id for subsequent verification calls
  • • Display SMS PIN entry UI immediately after success
  • • Show countdown timer for PIN expiration (10 minutes)
  • • Provide option to cancel send before verification
  • • Update sender balance display to show reserved amount
  • • Inform sender that receiver will be notified via SMS
  • • Handle both peer-to-peer and cross-game send flows