Approve in App

The sender approves a transfer or send from inside your app. The device polls the pending list, renders the prompt, and posts a hardware signature. For the complete numbered sequence, including the token minting this page assumes, see Peer Send and Cross-Game Transfer.

Before step 1: the player token

  • • Your server mints it with POST /api/sdk/player-token, the game secret, and player_email plus player_phone (a new player is registered; phone_mismatch: true means update the phone first, see Player Identity).
  • • If initiate answered identity_rekeyed: true, use the player_token it returned.
  • • Mint again right before step 2. On 403 PLAYER_TOKEN_STALE or 401 SDK_TOKEN_EXPIRED, mint and retry once.

1. Poll the pending list

GET

/api/sdk/transfers/pending

The authoritative list of actions awaiting this player

Poll this. There is no push to wire up

Call it on app open / login and periodically while foregrounded. Always render prompts from this response (amount, currency, whether a step-up is needed), never from anything cached. Authorization: Bearer <player token>

Response

{
  "pending": [
    {
      "transfer_id": "TX_abc123",
      "kind": "identity_gate",        // sender must approve
      "flow": "transfer",             // "transfer" → /transfers/{id}/approve
      "amount": "100.00",             //   "send"   → /send/{id}/approve
      "currency": "GoldCoins",
      "counterparty_game": "Game B",  // destination
      "expires_at": "2026-01-01T18:05:00Z",  // display only
      "step_up_required": false,      // only present when risk step-up is enabled for your game;
                                      //   if true, prompt biometric/PIN before approving
      "held": false,                  // true → not actionable yet (e.g. guardian pending)
      "hold_reason": null,
      "created_at": "2026-01-01T18:00:00Z",          // when the move was started
      "from_game": { "id": "123456789012", "name": "Game A" },  // where it started
      "fee_amount": "10.00",          // sender's item only; null on receiving_confirm
      "net_amount": "90.00",          // what arrives; sender's item only
      "usd_equivalent": "10.00"       // estimate of "amount" in USD, for display;
                                      //   null when no estimate is available
    }
  ]
}

Items with kind: "receiving_confirm" are value to collect. See Collecting, stage by stage. An item with held: true is awaiting something else, such as a guardian. Show it as pending, not approvable.

Amounts, fees and the net are in the item's own currency. On an approval you can show “fee 10, Game B receives 90” from fee_amount and net_amount. Label usd_equivalent as an estimate (“about $10.00”); never use it to charge or credit anything.

Read this before you build the "you have X to collect" screen

/pending is scoped to the logged-in player's token and returns exactly two kinds:

  • • identity_gate, a transfer/send this player initiated, awaiting their approval (sender view).
  • • receiving_confirm, value addressed to this player, awaiting their collect (receiver view). Covers both rails: a peer send arrives with flow: "send", a cross-game transfer with flow: "transfer".

Branch on flow, never on kind. kind tells you approve-versus-collect; flow tells you which endpoint to call. Reading receiving_confirm as one rail is how a collect screen ends up with no button on half the rows it is showing.

The one real trap: the sender and receiver are different tokens, even on a self-transfer where they are the same human. A token minted by the source title will not show the destination-side collect item, and vice-versa. Test each side with a token minted by thattitle. Full detail on the receiving side, both rails, is on Collecting, stage by stage.

Detecting completion (poll /status)

GET /api/sdk/transfers/pending is for discovery, it's the player's to-do list, authed with the player token. To detect that a specific transfer's sender approval has completed (so you can dismiss a waiting panel), poll that one transfer's /status instead.

GET
/api/transfers/{id}/status

Status of one self-transfer

GET
/api/currency-sends/{id}/status

Status of one player-to-player send

These are server-side endpoints. NOT the SDK player token

Unlike the /api/sdk/* calls, both /status endpoints are authed with your X-Game-Secret-Key from your server, never the device player token. Poll them server-side.

They return a verification_state that flips awaiting → approved once the approve call in step 2 has succeeded:

verification_stateMeaning
awaitingNobody has called approve yet. Keep the waiting panel up. If the player says they approved, your approve call never ran
approvedThe approve call succeeded. Dismiss the waiting panel
completedFully resolved (claimed / credited)
expiredWindow lapsed. Stop polling
failedDid not succeed. Stop polling
unknownA status the endpoint does not map to the five above. Read transaction_status for the detail

Polling /status moves no money. It reports what step 2 did. One integration polled and waited for approved without ever making the approve call, and every transaction expired. The transfer status and the send status also differ: only the send status carries completed_at, claimed_by_*, fees, game names and currency_name.

Use /status for a waiting panel keyed to one transfer; use /pending for discovery of what's awaiting the player.

2. Approve with a device signature

Pick the endpoint by the item's flow. The body is identical for both.

POST
/api/sdk/transfers/{id}/approve

Self-transfer (your own balance between your titles)

POST
/api/sdk/send/{id}/approve

Player-to-player send (sender side)

Authenticated with the player token

Authorization: Bearer <player token>

Request Body

ParameterTypeRequiredDescription
device_fingerprintstringYesWhich enrolled device is approving
device_signalobjectYesSignature over the action, see Device Enrollment
biometric_verifiedbooleanIf stepped upHonest result of the biometric / PIN step-up, when required

Success Response

{
  "status": "approved",
  "next": "pending_claim",
  "transaction_id": "TX_abc123"
}

Other responses to handle

StatusMeaning / what to do
202 STEP_UP_REQUIREDMay be returned when risk step-up is enabled for your title, prompt biometric / PIN, then retry with biometric_verified: true. A no-op when the flag is off.
202 RISK_HOLDMay be returned when risk step-up is enabled for your title, held for review; do not retry, show a pending state. A no-op when the flag is off.
202 pending_guardian_approvalAwaiting the guardian on a minor account. Show pending
410 GUARDIAN_APPROVAL_REJECTEDGuardian rejected the action. Stop; do not retry
410 GUARDIAN_APPROVAL_EXPIREDGuardian approval window lapsed. Stop; do not retry
503 GUARDIAN_APPROVAL_CHECK_UNAVAILABLETransient, the guardian check is temporarily unavailable; retry shortly
503 sdk_verification_disabledIn-app verification is switched off for the whole environment, not for your title. Retry later or contact Invo. (A title switched off on its own is refused earlier, at token mint, with 403 sdk_not_enabled_for_tenant / TENANT_NOT_MIGRATED.)
401 SDK_TOKEN_EXPIREDThe player token ran out. Mint a new one on your server and retry. Nothing moved
403 PLAYER_TOKEN_STALEIn code; error is not_send_sender / not_transfer_sender. The token predates the player’s phone being added. Mint a new one and retry once. Nothing moved
401 SIGNAL_*Bad/stale/replayed signature, re-sign with a fresh device_signal
400 (not pending)Already resolved. Treat as "already done"

After approval

The approve call is the one that moves money. A self-transfer moves to pending_claim and you claim it as usual. A send moves to pending_claim and the receiver is notified to confirm (Confirm Receipt). The downstream claim flow is unchanged.

Example: approve a transfer (Unity)

IEnumerator ApproveTransfer(string playerToken, string transferId,
    string deviceFingerprint, object deviceSignal, bool biometricPassed)
{
    // biometricPassed: true only if the OS biometric prompt actually succeeded on
    // this approval; false if it failed, was cancelled or was not shown.
    var url  = "https://invo.network/api/sdk/transfers/" + transferId + "/approve";
    var body = JsonConvert.SerializeObject(new {
        device_fingerprint = deviceFingerprint,
        device_signal      = deviceSignal,     // { transfer_id, nonce, timestamp, signature }
        biometric_verified = biometricPassed   // report it honestly
    });
    using (var req = new UnityWebRequest(url, "POST"))
    {
        req.uploadHandler   = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(body));
        req.downloadHandler = new DownloadHandlerBuffer();
        req.SetRequestHeader("Authorization", "Bearer " + playerToken);
        req.SetRequestHeader("Content-Type", "application/json");
        yield return req.SendWebRequest();
        // 202 STEP_UP_REQUIRED → run biometric, re-sign, retry.
    }
}

Report biometric_verified honestly. If the step-up fails or the user cancels, send false. Invo decides what happens. Never complete an action locally.