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 — 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, native Steam & desktop

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, and on a transfer it is also the only place the claim code is ever returned.

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. A legacy SMS PIN fallback also clears it; that fallback is being retired and new integrations should not build on it. 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_status": "pending_claim",
  "verification_state": "approved",          // awaiting | approved | completed | expired | failed
  "amount": "50.00", "net_amount": "45.00", "fee_amount": "5.00",
  "created_at": "...",
  "source_game_id": "...", "source_game_name": "...",
  "target_game_id": "...", "target_game_name": "...",
  "currency_id": 12, "currency_name": "Gold",
  "order_id": "TFRO_...",
  "claim_info": { "claim_code_expires_at": "..." }      // while collectable
}

Drive a waiting panel from verification_state: it flips from awaiting to approved the moment the sender clears the approval stage, by whichever factor. 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": 155963559928 }

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: rates can be configured per partner, and a locally computed total will disagree with the ledger the moment they are. 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 is paused for 24 hours after a self-serve passkey recovery. Show retry_after and stop; collecting is unaffected.
Platform-value containment409 STEAM_VALUE_NON_TRANSFERABLE / NON_STEAM_VALUE_INTO_STEAM_BLOCKEDValue that originated on a platform rail stays there. The body says how much is movable, offer that.
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)
transfer.sent / transfer.receivedCollected and creditedSource title / destination title
transfer.claim_expiredThe claim window lapsed without a collectBoth titles
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

Two older routes still work and are documented, and neither should appear in a new integration: the SMS PIN for the approval stage, and the claim code for collecting. Both are being retired.

A texted code is the most expensive channel we have and it authorises whoever reads the message. The passkey and QR paths cost nothing to deliver and authorise the person. That is the whole reason for the change.