What Your Server Must Call, In Order

One row per call, per flow, per side. Stage, who acts, endpoint, auth, body, what success looks like, and what to do on each error. Print it, tick it off, and you have a working integration. Full explanations live on Peer Send, Cross-Game Transfer and Collecting.

The two rows people skip

In both tables below, the approve row and the confirm-receipt row are the ones that move money. Everything above them produces a factor: a proof of who. A factor settles nothing. 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 tables

  • BASE is https://invo.network, or https://sandbox.invo.network/sandbox in sandbox.
  • game secret means the header X-Game-Secret-Key, server-side only.
  • player token means Authorization: Bearer <token>, scoped to one player in one game, 15 minutes, no refresh.
  • • Rows marked money are the settling calls.
  • • Rows marked optional are not required to complete a transaction.

Peer send, the sending side

#WhoEndpointAuthBodySuccessOn error
1Your serverPOST /api/currency-sends/initiate-sendgame secretclient_request_id, sender_player_name/_email/_phone, receiver_player_phone, receiving_game_id, amount; receiver_player_email recommended201 with transaction_id, verification_expires_at, new_balance202 = guardian gate, poll and continue. 403 PASSKEY_RECOVERY_COOLDOWN = stop, show retry_after. 409 duplicate = you already have this one. 429 = back off.
2Your serverPOST /api/sdk/player-tokengame secretplayer_email (the sender)200 with token, expires_at403 TENANT_NOT_MIGRATED = not switched on for your game, contact Invo. 404 = the player does not exist in your game.
3aServer (console / Steam)POST /api/sdk/approvals/device/beginsender’s player tokentransaction_id, flow: "send", channel (optional)200 with device_code, user_code, verification_uri_complete, expires_in, interval409 DEVICE_APPROVAL_ALREADY_PENDING = an approved grant exists, settle with the code you hold. 503 DEVICE_APPROVAL_NOT_CONFIGURED = contact Invo.
3bServer (console / Steam)POST /api/sdk/approvals/device/pollsender’s player tokendevice_code200 status: "approved". Now go to row 4authorization_pending = wait interval. slow_down = increase it. expired_token/invalid_grant = begin again. access_denied = stop.
3c optionalServerPOST /api/sdk/approvals/device/confirm-enrollmentsender’s player tokendevice_code, decision200 with the outcome; polling resumesOnly needed when a poll carried enrollment.state: "awaiting_screen". 409 = already decided elsewhere.
3dClient (web / mobile)POST /api/sdk/send/{id}/approve/webauthn/beginsender’s player tokennone200 challenge; run the ceremony400 WEBAUTHN_NO_CREDENTIAL = no passkey enrolled yet; enrol one or use the device grant.
4Server or clientPOST /api/sdk/send/{id}/approve moneysender’s player token{ device_code } or { webauthn_assertion } or { device_fingerprint, device_signal }200 { status: "approved", next: "pending_claim" }400 TRANSACTION_NOT_PENDING = already done, read current_status. 400 DEVICE_APPROVAL_NOT_APPROVED = wrong flow / wrong transaction / not approved. 400 SEND_APPROVE_FAILED = never retry, quote error_ref. 202/410 guardian.
5 optionalYour serverGET /api/currency-sends/{id}/statusgame secretnone200 with transaction_status, verification_state404 = not yours, or wrong rail. Prefer webhooks; use this to reconcile.
6 optionalYour serverPOST /api/sdk/send/{id}/resend-claimsender’s player tokennone200 { status: "resent", retry_after }429 RESEND_COOLDOWN = wait retry_after. 409 CLAIM_SMS_SUPPRESSED = they collect in-app; nothing to resend. 400 CLAIM_EXPIRED = start a new send.

Cross-game transfer, the source game

#WhoEndpointAuthBodySuccessOn error
1Your serverPOST /api/transfers/initiate-transfergame secretclient_request_id, source_player_name/_email/_phone, target_player_phone, target_game_id, amount; target_player_email recommended201 with transaction_id, verification_expires_at, new_balance202 = guardian gate. 403 TRANSFER_POLICY_VIOLATION = destination not permitted. 403 PASSKEY_RECOVERY_COOLDOWN = stop. 409 duplicate / consent / Steam containment.
2Your serverPOST /api/sdk/player-tokengame secretplayer_email (the source player)200 with tokenAs the send table, row 2.
3aServer (console / Steam)POST /api/sdk/approvals/device/beginplayer tokentransaction_id, flow: "transfer"200 with device_code, user_code, verification_uri_completeAs the send table, row 3a.
3bServer (console / Steam)POST /api/sdk/approvals/device/pollplayer tokendevice_code200 status: "approved". Now go to row 4As the send table, row 3b.
3cClient (web / mobile)POST /api/sdk/transfers/{id}/approve/webauthn/beginplayer tokennone200 challenge; run the ceremony400 WEBAUTHN_NO_CREDENTIAL = no passkey yet.
4Server or clientPOST /api/sdk/transfers/{id}/approve moneyplayer token{ device_code } or { webauthn_assertion } or { device_fingerprint, device_signal }200 { status, next, claim_code, claim_code_expires_at }. Store the claim code400 TRANSACTION_NOT_PENDING = already done (and the claim code is not re-issued). 400 TRANSFER_APPROVE_FAILED = never retry, quote error_ref. 202/410 guardian.
5 optionalYour serverGET /api/transfers/{id}/statusgame secretnone200 with transaction_status, verification_statePrefer webhooks; use this to reconcile.

There is no resend row for a transfer: the claim code came back to you in row 4.

Both flows, the receiving side

Identical for a peer send and a cross-game transfer except for the rail in the path, which is decided by the flow field on the pending item, never by assumption.

#WhoEndpointAuthBody / querySuccessOn error
1Your serverWebhook transfer.claim_pending (direction: inbound)signaturenoneYou know what arrived and for whom (to_phone, to_identity_id)Deduplicate on idempotency_key. Deliveries retry six times over ~31 hours.
1b optionalYour serverGET /api/transfers/inbound-pendinggame secretplayer_email or player_phone200 { inbound_pending: [...] }400 = neither query parameter given. Claim codes are never returned here.
2Your serverPOST /api/sdk/player-tokengame secret (receiving game)player_email (the recipient)200 with token404 = no player row yet. On a send use the hosted claim page; on a transfer use claim-transfer, because the hosted page serves sends only.
3ClientGET /api/sdk/transfers/pendingrecipient’s player tokennone200 { pending: [...] }. Take kind: "receiving_confirm" and read flowEmpty list = nothing awaiting this identity in this game. Do not infer the rail from kind.
4aClient (web / mobile)POST /api/sdk/{send|transfers}/{id}/confirm-receipt/webauthn/beginrecipient’s player tokennone200 challenge; run the ceremony400 WEBAUTHN_NO_CREDENTIAL = no passkey yet. 403 not_intended_receiver = wrong identity or phone mismatch.
4bServer (console / Steam)POST /api/sdk/approvals/device/begin then /pollrecipient’s player tokentransaction_id, flow: "send_receipt" or "transfer_receipt"device_code, then status: "approved"Same RFC 8628 codes as the sending side. Using the sender’s flow here fails at row 5.
5Server or clientPOST /api/sdk/{send|transfers}/{id}/confirm-receipt moneyrecipient’s player token{ device_code } or { webauthn_assertion } or { device_fingerprint, device_signal }200 { status: "completed", amount_received }409 receiver_not_enrolled_use_claim_code = send → hosted claim page or claim-currency; transferclaim-transfer only. 409 PHONE_SHARE_APPROVAL_REQUIRED (under error_code) = consent, then retry. 202 RECIPIENT_IDENTITY_PENDING = wait, then retry. 400 TRANSACTION_NOT_PENDING = already collected. 400 CONFIRM_RECEIPT_FAILED = never retry, never refund; read the status, then quote error_ref.
6 optionalYour serverWebhook transfer.received, or GET /api/{currency-sends|transfers}/{id}/statussignature / game secretnoneCredited, with amount_received and new_balanceAlso fires for collects made outside your game, such as the hosted page or the Invo app. Handle it even if your server made no call.

The legacy rows, kept out of the tables on purpose

These endpoints still work and are documented, but they are the routes being retired. They are not steps in a new integration, which is why they are not rows above.

Legacy routeReplaced byWhy
POST /api/currency-sends/verify-sms
POST /api/transfers/verify-sms
Row 4, approve with a passkey or device grantA texted PIN is the most expensive and least secure proof available. Being retired.
POST /api/currency-sends/resend-pin
POST /api/transfers/resend-pin
Nothing, there is no PIN to resend on the passkey pathExists only to support the legacy PIN route.
POST /api/currency-sends/claim-currency
POST /api/transfers/claim-transfer
Receiving row 5, confirm-receipt; or the hosted claim pageA code authorises whoever holds it. Kept for a recipient with no Invo passkey, and because it creates the player row.

Next

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