What Your Server Must Call, In Order

The numbered steps from Peer Send and Cross-Game Transfer, as a tick-list. Work down it; every box is a call you make or a check you do. The receiving side is detailed on Collecting.

The two boxes people skip

The green boxes, approve and confirm-receipt, are the only calls that move money. Everything before them produces a proof of who, and a status read only reports. If your integration ends at “the poll said approved”, every transaction you create will expire and refund, and nothing in your logs will look like an error.

Reading the list

  • • BASE is https://invo.network, or https://sandbox.invo.network/sandbox in sandbox.
  • • SERVER calls carry X-Game-Secret-Key, server-side only.
  • • CLIENT calls, and every /api/sdk/* call except player-token, carry Authorization: Bearer <player token>: one player, one title, short-lived, no refresh.
  • • There is nothing to configure for passkeys: the approval always runs on INVO’s own page.

Peer send, the sending side

  • 1.SERVERPOST /api/sdk/player-token, game secret
    Body has player_email and player_phone (+ player_name). created: true = registered now. phone_mismatch: true = update the phone with POST /api/sdk/players/phone (Player Identity) and mint again; 409 PHONE_CHANGE_REQUIRES_VERIFICATION there = the player has a verified factor, nothing changed, keep the current number and contact INVO support. 404 player_not_found with hint: "pass player_phone to create the player" = you left out the phone: mint again with it, or register first with POST /api/sdk/players.
  • 2.SERVERPOST /api/currency-sends/initiate-send, game secret
    Stored transaction_id. If identity_rekeyed: true, switched to the returned player_token. 202 = guardian gate, wait for it. 403 PASSKEY_RECOVERY_COOLDOWN = stop, show retry_after. Read claim_delivery_planned ("email" | "none" | legacy "sms") and tell the sender how the receiver will collect. receiver_player_email is optional and recommended.
  • 3.SERVERMint the sender’s token again, right before approval
    The client gets a fresh token (or step 2’s player_token), never one minted at login.
  • 4.CLIENT or SERVERThe sender approves, flow: "send"
    Web: approveHosted({ transactionId, flow: "send" }) from a click (it also runs step 5). Consoles, desktop launchers, popup blocked: POST /api/sdk/approvals/device/begin, show the QR, POST /api/sdk/approvals/device/poll until "approved". Mobile app: the plugin’s hosted approval.
  • 5.SERVER or SDKmoves moneyPOST /api/sdk/send/{id}/approve, sender’s player token
    Sent { device_code }. Got 200 { status: "approved", next: "pending_claim", claim_delivery }. claim_delivery: "email" = the claim link went to the receiver’s email already on file for that phone; "none" = nothing sent, the receiver collects in the game; "sms" = legacy. PLAYER_TOKEN_STALE / SDK_TOKEN_EXPIRED = mint, retry once. TRANSACTION_NOT_PENDING = already done. SEND_APPROVE_FAILED = never retry, read status, quote error_ref.
  • 6.SERVERWebhook transfer.claim_pending, or GET /api/currency-sends/{id}/status
    verification_state: "approved". Still "awaiting" after the player approved = step 5 was never called.
  • 6b.CLIENToptionalPOST /api/sdk/send/{id}/resend-claim, sender’s player token
    Live; re-delivers the claim link on the original channel. 200 carries claim_delivery. 409 CLAIM_DELIVERY_WITHHELD = the receiver email is not on file for that phone, nothing is sent, the receiver collects in the game: do not retry. 429 RESEND_COOLDOWN = wait retry_after.

Cross-game transfer, the source title

  • 1.SERVERPOST /api/sdk/player-token, source game secret
    Body has player_email and player_phone (+ player_name). Handle created, phone_mismatch and 404 as for a send.
  • 2.SERVERPOST /api/transfers/initiate-transfer, source game secret
    Stored transaction_id. If identity_rekeyed: true, switched to the returned player_token. 403 TRANSFER_POLICY_VIOLATION = destination not permitted.
  • 3.SERVERMint the player’s token again, right before approval
    Fresh token to the client (or step 2’s player_token).
  • 4.CLIENT or SERVERThe player approves, flow: "transfer"
    Web: approveHosted({ transactionId, flow: "transfer" }) from a click. Consoles, desktop launchers, popup blocked: QR device grant, poll to "approved". Mobile app: the plugin’s hosted approval.
  • 5.SERVER or SDKmoves moneyPOST /api/sdk/transfers/{id}/approve, player token
    Got 200 with claim_code, claim_code_expires_at and claim_delivery, and stored both the code and its expiry (approve also emails the code to the sender’s own address on file when claim_delivery is "email"). Stale or expired token = mint, retry once. TRANSFER_APPROVE_FAILED = never retry.
  • 6.SERVERWebhook transfer.claim_pending, or GET /api/transfers/{id}/status
    verification_state: "approved". With the source secret the status also returns the claim code. It has no fee, game-name or completed_at fields: amounts come from transfer.received.

The claim code came back to you in step 5, and the source title can read it again from the status endpoint.

Both flows, the receiving side

  • 7.RECEIVING SERVERPOST /api/sdk/player-token, receiving game secret
    Recipient’s email and phone, so a first-time recipient is registered there.
  • 8.RECEIVING CLIENTGET /api/sdk/transfers/pending, recipient’s token
    Took the item with kind: "receiving_confirm" and branched on its flow (send or transfer), never on kind.
  • 9a.RECEIVING CLIENT or SERVERThe recipient approves, flow: "send_receipt" or "transfer_receipt"
    approveHosted() on the web, QR grant elsewhere. A sender’s flow here fails as DEVICE_APPROVAL_NOT_APPROVED.
  • 9b.RECEIVING SERVER or SDKmoves moneyPOST /api/sdk/{send|transfers}/{id}/confirm-receipt, recipient’s token
    Got 200 { status: "completed", amount_received }. 409 receiver_not_enrolled_use_claim_code: send = hosted claim page or claim-currency; transfer = claim-transfer. CONFIRM_RECEIPT_FAILED = never retry.
  • 10.SERVERWebhooks transfer.sent and transfer.received
    Both handled, including collects made outside your title (hosted page, Invo app). Deduplicate on idempotency_key.

The claim-code fallback, kept off the list on purpose

These endpoints still work, but they are a fallback, not steps in a new integration. Approval is always the hosted page or a QR grant; any code goes by email.

Legacy routeReplaced byNotes
POST /api/currency-sends/claim-currency
POST /api/transfers/claim-transfer
Step 9b, confirm-receipt; or the hosted claim page (sends only)A code authorises whoever holds it. Kept for a recipient who cannot use a passkey.

Next

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