Claim Sent Currency
Complete a currency send by claiming the funds with a claim code received via SMS. This endpoint validates the claim code, creates or updates the receiver player, and credits the sent currency to their balance.
/api/currency-sends/claim-currency
Claims a currency send using the provided claim code
Authentication Required
Must include the receiving game's secret key (where the funds will be received):
X-Game-Secret-Key: receiving_game_secret_key_hereRequest Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| claim_code | string | Yes | Claim code received via SMS. Format isABCDE-12345— 5 uppercase letters (excluding O) + hyphen + 5 digits (excluding 0). Total 11 characters. Case-sensitive. |
| receiver_player_name | string | Yes | Display name of the player claiming the currency |
| receiver_player_email | string | Yes | Email address of the receiving player |
| receiver_player_phone | string (E.164) | Yes | 🔒 SECURITY: Phone of the receiving player. MUST match the receiver_player_phone sent at initiation. Strict E.164 — leading + is required (e.g., +15551234567). Without the + the request returns 400 "Phone number must start with country code." See callout below. |
Phone format — strict E.164
receiver_player_phone is validated as strict E.164: a leading + followed by 10–15 digits. Common formatting ((555) 123-4567, spaces, dashes) is stripped server-side, but the + itself is required. Without it the backend returns:
{"status": "error", "message": "Phone number must start with country code (e.g., +1234567890)"}The backend will not auto-prepend a default country code (e.g., assuming 5551234567 means +1) because the universal player identity hash and cross-tenant phone-binding security check require phones to canonicalize identically across tenants — guessing would silently misalign US and Canadian numbers with the same trailing digits.
Your UI should canonicalize before submit. Either use a phone-input component that always emits E.164 (react-phone-number-input, libphonenumber-js), or normalize manually:
function toE164(input, defaultCountry = 'US') {
if (!input) return null;
if (input.startsWith('+')) return input.replace(/[^\d+]/g, '');
const digits = input.replace(/\D/g, '');
if (defaultCountry === 'US' && digits.length === 10) return `+1${digits}`;
if (defaultCountry === 'US' && digits.length === 11 && digits[0] === '1') return `+${digits}`;
return null; // ambiguous — surface a 'include country code' hint
}The same rule applies to every phone field on every transfer/send endpoint (source_player_phone, target_player_phone, receiver_player_phone) — fix it once at your input layer.
Claim Code Requirements
Code Format
- • Format:
ABCDE-12345(5 letters · hyphen · 5 digits, 11 chars total) - • Uppercase letters only, excluding
O(to avoid 0/O confusion) - • Digits 1–9 only, excluding
0(same reason) - • Case-sensitive, unique per currency send, one-time use
- • Example:
KJMRS-47281
Time Limits
- • Code expires in 24 hours
- • Maximum 5 claim attempts
- • Failed attempts count toward rate limits
- • Expired codes cannot be reused
How to Get a Claim Code
SMS Notification Process
When someone sends you currency, you'll automatically receive an SMS notification with all the details you need to claim it:
"You have received 450.00 Gold from PlayerOne. Claim code: KJMRS-47281. Use this in Space Warriors. Expires in 24 hours."
- • Amount & Currency: How much currency you're receiving
- • Sender Name: Who sent the currency to you
- • Claim Code: The code you'll enter to claim the currency
- • Target Game: Which game to claim the currency in
- • Expiration: 24-hour time limit to claim
Implementation Examples
UnityUnity C# Currency Claim
// This function MUST be called on your secure server, NOT the client.
public IEnumerator ClaimSentCurrency(string claimCode, string receiverName,
string receiverEmail, string receiverPhone,
Action<string> onSuccess, Action<string> onError)
{
string url = "https://invo.network/api/currency-sends/claim-currency";
var requestData = new {
claim_code = claimCode,
receiver_player_name = receiverName,
receiver_player_email = receiverEmail,
receiver_player_phone = receiverPhone
};
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", "receiving_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 Claim
// This function MUST be called on your secure server.
void AYourGameMode::ClaimSentCurrency(const FString& ClaimCode,
const FString& ReceiverName, const FString& ReceiverEmail,
const FString& ReceiverPhone)
{
TSharedPtr<FJsonObject> RequestObj = MakeShareable(new FJsonObject);
RequestObj->SetStringField("claim_code", ClaimCode);
RequestObj->SetStringField("receiver_player_name", ReceiverName);
RequestObj->SetStringField("receiver_player_email", ReceiverEmail);
RequestObj->SetStringField("receiver_player_phone", ReceiverPhone);
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/claim-currency");
Request->SetVerb("POST");
Request->SetHeader("X-Game-Secret-Key", "receiving_game_secret_key_here");
Request->SetHeader("Content-Type", "application/json");
Request->SetContentAsString(RequestBody);
Request->OnProcessRequestComplete().BindUObject(this, &AYourGameMode::OnCurrencyClaimed);
Request->ProcessRequest();
}GodotGodot GDScript Currency Claim
# This function MUST be called on your secure server.
func claim_sent_currency(claim_code: String, receiver_name: String,
receiver_email: String, receiver_phone: String):
var url = "https://invo.network/api/currency-sends/claim-currency"
var headers = [
"X-Game-Secret-Key: receiving_game_secret_key_here",
"Content-Type: application/json"
]
var body = {
"claim_code": claim_code,
"receiver_player_name": receiver_name,
"receiver_player_email": receiver_email,
"receiver_player_phone": receiver_phone
}
http_request.request(url, headers, HTTPClient.METHOD_POST, JSON.stringify(body))
Request & Response Examples
Example Request
POST /api/currency-sends/claim-currency
Content-Type: application/json
X-Game-Secret-Key: receiving_game_secret_key_here
{
"claim_code": "KJMRS-47281",
"receiver_player_name": "PlayerTwo",
"receiver_player_email": "player2@example.com",
"receiver_player_phone": "+1987654321"
}Success Response (200 OK)
{
"status": "success",
"message": "Currency send claimed successfully.",
"transaction_id": "txn_send123abc456",
"send_details": {
"amount_received": "450.00",
"sending_game": "Adventure Quest",
"receiving_currency": "Gold",
"receiver_player": "PlayerTwo",
"new_balance": "1450.00"
},
"completion_time": "2024-06-24T15:30:00Z",
"order_id": "ord_send789def012"
}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 receiver's email is new on a phone that's 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 claim. Only call /phone-share/initiate manually if code_sent: false. Match on error_code, not message text.
If already_approved: true: auto-retry the claim 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."
Two equally valid approval paths the user can choose between: (1) type the 6-digit code into your UI → call POST /api/wallet/phone-share/approve, or (2) text the code back from their phone (Invo's inbound webhook approves server-side). If a user takes path 2, your client gets no direct callback — poll GET /api/wallet/phone-share/status?phone=...&email=... every 3–5s while your panel is open and auto-retry the claim when approved: true.
Account Selection — Multi-Email-on-Phone (200 OK with picker shape)
{
"status": "needs_account_selection",
"claim_code": "ABCDE-12345",
"message": "Multiple accounts share this phone number on the destination game and the supplied email did not match any of them. Re-submit with receiver_player_id set to the chosen account, or fix the receiver_player_email.",
"candidates": [
{ "player_id": 12345, "email_hint": "al***@example.com" },
{ "player_id": 67890, "email_hint": "bo***@example.com" }
]
}Why this fires: Invo's wallet model supports multiple emails on one verified phone (parent + child, two-account gamers, family-shared device). When the destination game has 2+ Player rows on the same phone, the claim endpoint resolves which one to credit using this priority order:
receiver_player_idin the request body (must match a candidate) — your client's explicit picker selection.receiver_player_emailmatches exactly one candidate's email — the common case. The user already typed their email into your claim form, so we don't prompt twice. No picker fires.- Picker response (the 200 above) — only when steps 1 and 2 both fail.
Recovery: render a picker from candidates showing each email_hint, let the user tap one, then re-submit /claim-currency with the same body PLUS receiver_player_id set to the chosen player_id. Status code is 200 not an error — the call hasn't failed, it just needs disambiguation.
Most claims never see this response. It surfaces only in genuinely ambiguous cases (e.g., the user typo'd their email). Implementing the picker UI is a low-effort defensive measure — same shape applies to /claim-transfer.
Error Response - Invalid Claim Code (400 Bad Request)
{
"status": "error",
"message": "Invalid claim code.",
"attempts_remaining": 4
}Error Response - Expired Code (400 Bad Request)
{
"status": "error",
"message": "Claim code is not valid: Claim code expired."
}Error Response - Send Not Found (404 Not Found)
{
"status": "error",
"message": "Invalid claim code or currency send not intended for this game."
}Error Response - Wrong Claim Endpoint (400 Bad Request)
Returned when a claim code exists for this game but was minted by the cross-game self-transfer flow rather than the player-to-player send flow. Switch your call to /api/transfers/claim-transfer with target_* field names + target_currency_id.
{
"status": "error",
"error_code": "WRONG_CLAIM_ENDPOINT",
"message": "This claim code is for a self-transfer. Use POST /api/transfers/claim-transfer instead.",
"expected_endpoint": "/api/transfers/claim-transfer",
"expected_body_fields": [
"claim_code",
"target_player_name",
"target_player_email",
"target_player_phone",
"target_currency_id"
]
}Switch on error_code === "WRONG_CLAIM_ENDPOINT" in your handler — message text may evolve, but the code and expected_endpoint field are stable contract.
Error Response - Phone Number Mismatch (403 Forbidden)
{
"status": "error",
"message": "Phone number mismatch: This claim code can only be redeemed by the intended recipient's phone number."
}Error Response - Claim Attempt Lockout (429 Too Many Requests)
Shipped 2026-05-14. Every failed claim attempt — wrong code, wrong phone, wrong endpoint, wrong PIN — writes an audit row keyed on phone + email + IP. When any dimension trips its trailing-window threshold, the endpoint returns 429 with error_code: "CLAIM_LOCKED". Match on the code, not message text.
HTTP/1.1 429 Too Many Requests
Retry-After: 1800
{
"status": "error",
"error_code": "CLAIM_LOCKED",
"error": "invalid_claim_code_blocked",
"message": "Too many failed claim attempts for this phone number. Please wait 30 minutes before trying again.",
"locked_dimension": "phone",
"retry_after_seconds": 1800,
"retry_after": 1800
}Two equivalent ways to match: new integrations should switch on error_code === "CLAIM_LOCKED". Existing integrations whose security-block handler keys on the legacy field can switch on error === "invalid_claim_code_blocked" — both fields are present on every lockout response. Same goes for the duration field: retry_after_seconds (canonical) and retry_after (legacy alias) carry the same value.
| Dimension | Threshold | Window | Lockout |
|---|---|---|---|
receiver_player_phone | 5 failures | trailing 30 min | 30 min |
receiver_player_email | 5 failures | trailing 30 min | 30 min |
ip_address | 20 failures | trailing 60 min | 60 min |
Read retry_after_seconds (also in the Retry-After header) to drive your back-off. locked_dimension is informational — your UI should treat all three the same way (don't leak which dimension tripped). locked_dimension: "db_unavailable" indicates a transient Invo-side issue with retry_after_seconds: 60; retry shortly.
🔒 BREAKING CHANGE: Phone-Based Security
As of the latest update, claim codes can ONLY be redeemed by the exact phone number specified during send initiation via receiver_player_phone.
- • The
receiver_player_phoneprovided during claim MUST exactly match thereceiver_player_phonefrom initiation - • Phone mismatch results in 403 Forbidden error
- • This prevents unauthorized claim code sharing and redemption
- • Enhances security for player-to-player currency sends
What Happens During a Successful Claim
Currency Credited
The net send amount (original amount minus fees) is immediately credited to the receiver's balance in the receiving game. If the player doesn't exist in the receiving game, a new player account is automatically created.
Send Completed
The send status is updated to "completed" and the transaction is finalized. The sender's reserved funds are permanently deducted, and fee distributions are recorded for all participating games and the platform.
SMS Confirmation
A confirmation SMS is sent to the receiver's phone number confirming the successful claim:
"Invo Currency Claimed: You have successfully claimed 450.00 Gold from a currency send via Adventure Quest."
Implementation Best Practices
User Experience
- • Provide clear claim code input validation and formatting hints
- • Display remaining attempts when claim fails
- • Show clear success messages with amount received
- • Update player balance immediately in UI after successful claim
- • Pre-populate player fields if user is logged in
- • Show sender information clearly (who sent the currency)
Error Handling
- • Parse error responses to show user-friendly messages
- • Handle rate limiting gracefully with retry suggestions
- • Differentiate between expired codes and invalid codes
- • Log claim attempts for customer support purposes
- • Implement proper timeout handling for network requests
Security
- • Never log or store claim codes in your application
- • Validate claim code format before API submission
- • Clear claim code input fields after processing
- • Use HTTPS for all API communications
- • Validate phone number format on client side
Player Onboarding
- • Validate email format before submission
- • Ensure phone numbers include country codes
- • Provide helpful onboarding for new players claiming their first currency
- • Confirm player details before final claim submission
- • Explain what happens after successful claim
- • Guide new players through account creation process