Transfer Overview

A cross-game transfer moves a player’s own currency out of your title and into another title in the Invo network. It is a self-transfer: the same person is on both ends. They authorise it in your title with a passkey, or by approving on their phone from a QR code where the client cannot run a passkey, and then collect it at the destination.

The shape of it

SOURCE GAME                                     DESTINATION GAME
1  initiate-transfer          [game secret]
     amount reserved, approval window opens
2  the player proves it is them, with ONE factor:
     passkey in-client, or a QR device approval
3  approve                    [player token]     <-- the call that moves money
     -> pending_claim, and the claim code
                                                4  the player collects there:
                                                     pending list -> confirm-receipt [their token]
                                                     (claim code is the fallback)
5  transfer.sent                                  transfer.received

Web, mobile web, mobile app

The passkey runs in the client. Face ID, Touch ID, Windows Hello. Strongest option, nothing to deliver, nothing to type.

Consoles, TVs & desktop launchers

The client cannot invoke the platform authenticator, so the ceremony moves to the player’s phone via a QR code and your server polls for the result.

A factor is not a settlement

Whatever the player used to prove it was them is a factor. It settles nothing. The transfer only advances when your side calls, with the player’s session token:

POST /api/sdk/transfers/{transaction_id}/approve
Authorization: Bearer <the player's session token>
{ "device_code": "..." }   // or { "webauthn_assertion": { ... } }

A device-approval poll that answers approved is the cue to make that call, not the end of the flow. On a transfer the approve response is also where the claim code is issued; afterwards the sending game can read it from GET /api/transfers/{id}/status, and approve emails it to the sender’s own bound address.

Where the detail lives

Statuses

transaction_statusverification_stateMeaning
pending_pin_verificationawaitingReserved on the sender’s balance, waiting for them to approve. (The name is historical; see the note below.)
pending_claimapprovedApproved. Waiting to be collected at the destination.
completedcompletedCredited at the destination. Terminal.
expired_pin_verificationexpiredNever approved inside the approval window; refunded to the sender.
expired_claimexpiredApproved but never collected; refunded to the sender.

Why the status says “pin”. The name is historical. It is the sender approval stage, and today it is cleared by a passkey or a device approval grant. Treat the string as an opaque value meaning “waiting for the sender to approve”.

Reading a transfer’s state

GET $BASE/api/transfers/{transaction_id}/status
X-Game-Secret-Key: <game secret of either game>

200 OK
{
  "status": "success",
  "transaction_id": "TXN_...",
  "transaction_type": "transfer",
  "transaction_status": "pending_claim",
  "verification_state": "approved",   // awaiting | approved | completed | expired | failed | unknown
  "amount": "50.00", "net_amount": "45.00",
  "created_at": "...",
  "source_game_id": "...",
  "target_game_id": "...",
  "currency_id": 12,
  "order_id": "TFRO_...",
  "claim_code": "...",                // source title only, once approved; null otherwise
  "claim_code_expires_at": "...",     // source title only, once approved; null otherwise
  "financial_tracking_status": "..."
  // additive: "expiry_reason" ("approval_expired" | "pin_expired" | "claim_expired"),
  //           present only when the transfer expired
}

Drive a waiting panel from verification_state: it flips from awaiting to approved the moment the sender clears the approval stage, by whichever factor (unknown covers any status the endpoint does not map; read transaction_status for the detail). The destination title also receives to_phone and to_identity_id on this response, for attributing the inbound transfer to one of its players.

Prefer webhooks for your ledger and this endpoint for reconciliation. See Lifecycle & webhooks.

Where a player may transfer to

A transfer’s destination must be a title your transfer policy permits and that is live. Ask before you offer, rather than discovering it as a 403 TRANSFER_POLICY_VIOLATION at initiate:

POST $BASE/api/transfers/available-destinations
X-Game-Secret-Key: <game secret>
{ "source_game_id": 123456789012 }

The full response contract, including per-destination limits and the transfer-policy fields, is on Available Player Destinations. For a list narrowed to where a specific player can actually receive, see Player-Aware Destinations.

Fees

Fees are calculated and stamped on the transaction at initiate, and nothing recomputes them later. fees_preview on the initiate response is what will be charged, and the collect side reports the same numbers in fee_breakdown on transfer.received.

Always read the numbers off the response. Never re-derive a fee from a rate in your own code. The split is a network standard (10% in total: 3.5% to the sending title, 3.5% to the receiving title and 3% to INVO; a same-title move keeps both title shares), the same for every partner and not configurable, but the stamped figures are the ledger's, so read them rather than recomputing. net_amount is what lands.

Guardrails you will meet

GuardrailHow it appearsWhat to do
Guardian approval (minor accounts)202 at initiate with a guardian_approval block; 202 GUARDIAN_APPROVAL_PENDING at approvePoll the approval-status endpoint, then approve as normal. The request goes to the guardian by email where one is verified. See Guardian Approval.
Phone-share consent409 PHONE_SHARE_APPROVAL_REQUIREDThe number is already tied to a different Invo identity. Follow the consent flow in the body, then retry. Consent is requested by email where a verified address exists.
Post-recovery hold403 PASSKEY_RECOVERY_COOLDOWN at initiate and approveMoney out may be paused for a while after an account recovery. The response says until when (retry_after): show it and stop. Collecting is unaffected.
Platform-value containment409 NON_STEAM_VALUE_INTO_STEAM_BLOCKED, 409 STEAM_VALUE_NON_TRANSFERABLEA title distributed through a platform store cannot receive currency from a title outside that store. Currency bought through a platform store is subject to that store's rules and can only move between titles on that store. Destination lists already leave out titles you cannot send into.
Velocity and rate limits429, usually with retry_afterBack off. Do not loop.
Maintenance pause503 flow_pausedTransfers are paused platform-wide. Show a temporary state and retry later.

Every code, with its meaning and required action, is on the error reference.

What you are told, and when

EventFires whenDelivered to
device_approval.approvedA QR approval settled on the player’s phone, the cue to call approveThe title that began the grant
transfer.claim_pendingThe sender approved; there is something to collectBoth titles (direction outbound / inbound); a same-title move delivers both copies to that title
transfer.sent / transfer.receivedCollected and creditedSource title / destination title
transfer.claim_expiredThe claim window lapsed without a collect; carries expiry_reasonBoth titles, as above
transfer.refundedReserved funds returned to the sender, on either expiry pathSource title

Peer sends use the same transfer.* names, they are not a separate event family. Setup and signature verification are on Receiving Webhooks.

Legacy fallbacks

An older collecting route still works and is documented, and should not appear in a new integration: the claim code. It is being retired.

A code anyone can read authorises whoever reads it. The passkey and QR paths cost nothing to deliver and authorise the person. That is the whole reason for the change.