Send Overview

A peer send is one player giving currency to another, addressed by phone number, inside your game or platform, or into a different one. Two people, two sides. The sender authorises it with a passkey, or by approving on their phone from a QR code where the client cannot run a passkey. The recipient collects with their own proof, the same way.

The shape of it

SENDING GAME                                    RECEIVING GAME (may be the same game)
1  initiate-send              [game secret]
     amount reserved, approval window opens
2  the sender 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
                                                4  the recipient collects:
                                                     pending list -> confirm-receipt [their token]
                                                     or the hosted claim page if they have no account
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 sender used to prove it was them is a factor. It settles nothing. The send only advances when your side calls, with the sender’s session token:

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

The same is true on the receiving side: an approved receipt grant does nothing until you call confirm-receipt with it.

Where the detail lives

Send or transfer?

Peer sendCross-game transfer
Who is on each endTwo different peopleThe same person, twice
Paths/api/currency-sends/…, /api/sdk/send/…/api/transfers/…, /api/sdk/transfers/…
Device-grant flowsend / send_receipttransfer / transfer_receipt
Who is notified there is something to collectThe recipient, in-app where possible, otherwise a link to the hosted claim pageNobody needs telling: the claim code returns in-band to the caller at approve
Can the sender re-notify?Yes, /api/sdk/send/{id}/resend-claimNot applicable

A same-game send is still a send: set receiving_game_id to your own game id, and the response reports send_type: "peer_to_peer".

Statuses

transaction_statusverification_stateMeaning
pending_pin_verificationawaitingReserved on the sender’s balance, waiting for them to approve.
pending_claimapprovedApproved. Waiting for the recipient to collect.
completedcompletedCredited to the recipient. 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 send’s state

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

200 OK
{
  "status": "success",
  "transaction_id": "TXN_...",
  "transaction_type": "currency_send",
  "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": "...",
  "sending_game_id": "...", "sending_game_name": "...",
  "receiving_game_id": "...", "receiving_game_name": "...",
  "currency_id": 12, "currency_name": "Gold",
  "order_id": "ORD_1757000000_A1B2C3D4",     // string, not a number
  "claim_info": { "claim_code_expires_at": "..." }      // while collectable
}

The receiving game also gets to_phone and to_identity_id on this response, for attributing the inbound send to one of its players. To enumerate everything waiting for a player without knowing transaction ids, use GET /api/transfers/inbound-pending. See Collecting.

“They say they never got it”

Once a send is approved, the sender can re-deliver the recipient’s collect notification. Nothing new is minted, the identical message goes out again, so support can read the two side by side.

POST $BASE/api/sdk/send/{transaction_id}/resend-claim
Authorization: Bearer <the SENDER's player token>       (no body)

200 { "status": "resent", "transaction_id": "TXN_...", "retry_after": 30 }
429 RESEND_COOLDOWN          too soon; wait retry_after seconds
409 CLAIM_SMS_SUPPRESSED     this recipient collects in-app; nothing is sent for this send
400 NOT_CLAIMABLE            not awaiting a claim
400 CLAIM_EXPIRED            the window closed; start a new send

Note this is the recipient’s notification. It is a different thing from the legacy /api/currency-sends/resend-pin, which belongs to the retiring SMS PIN path.

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_REQUIRED, at initiate or at collectThe 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.
Velocity and rate limits429, usually with retry_afterBack off. Do not loop.
Maintenance pause503 flow_pausedSends 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

A peer send emits the same transfer.* events as a cross-game transfer, there is no separate send event family.

EventFires whenDelivered to
device_approval.approvedA QR approval settled on the player’s phone, the cue to call approve or confirm-receiptThe title that began the grant
transfer.claim_pendingThe sender approved; there is something to collectBoth games (direction outbound / inbound)
transfer.sent / transfer.receivedCollected and credited. The send rail's payload carries flow: "currency_send" and no gross_amount or fee_breakdownSending game / receiving game
transfer.claim_expiredThe claim window lapsed without a collectBoth games
transfer.refundedReserved funds returned to the sender, on either expiry pathSending game

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.

The one message still sent as a matter of course is the collect link to a recipient we can reach no other way, a phone number is all the sender gave us, so there is no address to email and no app to notify. Where the recipient is reachable in-app, even that is suppressed.