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, andplayer_emailplusplayer_phone(a new player is registered;phone_mismatch: truemeans update the phone first, see Player Identity). - • If initiate answered
identity_rekeyed: true, use theplayer_tokenit returned. - • Mint again right before step 2. On
403 PLAYER_TOKEN_STALEor401 SDK_TOKEN_EXPIRED, mint and retry once.
1. Poll the pending list
/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 withflow: "send", a cross-game transfer withflow: "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.
/api/transfers/{id}/statusStatus of one self-transfer
/api/currency-sends/{id}/statusStatus 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_state | Meaning |
|---|---|
| awaiting | Nobody has called approve yet. Keep the waiting panel up. If the player says they approved, your approve call never ran |
| approved | The approve call succeeded. Dismiss the waiting panel |
| completed | Fully resolved (claimed / credited) |
| expired | Window lapsed. Stop polling |
| failed | Did not succeed. Stop polling |
| unknown | A 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.
/api/sdk/transfers/{id}/approveSelf-transfer (your own balance between your titles)
/api/sdk/send/{id}/approvePlayer-to-player send (sender side)
Authenticated with the player token
Authorization: Bearer <player token>Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| device_fingerprint | string | Yes | Which enrolled device is approving |
| device_signal | object | Yes | Signature over the action, see Device Enrollment |
| biometric_verified | boolean | If stepped up | Honest result of the biometric / PIN step-up, when required |
Success Response
{
"status": "approved",
"next": "pending_claim",
"transaction_id": "TX_abc123"
}Other responses to handle
| Status | Meaning / what to do |
|---|---|
| 202 STEP_UP_REQUIRED | May 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_HOLD | May 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_approval | Awaiting the guardian on a minor account. Show pending |
| 410 GUARDIAN_APPROVAL_REJECTED | Guardian rejected the action. Stop; do not retry |
| 410 GUARDIAN_APPROVAL_EXPIRED | Guardian approval window lapsed. Stop; do not retry |
| 503 GUARDIAN_APPROVAL_CHECK_UNAVAILABLE | Transient, the guardian check is temporarily unavailable; retry shortly |
| 503 sdk_verification_disabled | In-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_EXPIRED | The player token ran out. Mint a new one on your server and retry. Nothing moved |
| 403 PLAYER_TOKEN_STALE | In 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.