In-App Verification
The modern way to verify cross-game transfers and player-to-player sends, the player approves right inside your app with a hardware-backed signature, instead of typing an SMS PIN. It is faster, dramatically reduces SMS costs, and resists SIM-swap attacks that a texted code cannot.
Why in-app verification
SIM-swap resistant
Approval is a signature from a key sealed in the device's secure hardware, it can't be intercepted or forwarded like an SMS code.
Fewer SMS, lower cost
The in-app prompt replaces the per-step verification text. SMS stays only as a fallback for players who don't have the app.
Seamless UX
The player taps approve in your title, no leaving the app to read a text and type a code back.
Additive and opt-in, nothing you have today breaks
In-app verification lives under /api/sdk/* and is the approval pathfor both money flows: initiate → approve → confirm-receipt. Your existing initiate and claim integration is unchanged; the middle step is what moves to a passkey.
It is enabled per title, coordinated with you once your plugin is live and tested. Until then a title runs on the legacy SMS PIN fallback, which is being retired, and which no new integration should be built on.
A client that cannot run a passkey is not stuck on messaging. Consoles, TVs and native Steam or desktop builds use the QR device approval grant instead: the ceremony happens on the player's phone, and your server settles with the same approve call. Worked examples in curl, JavaScript and Python are on Peer Send, Cross-Game Transfer and Collecting.
On the web instead of a mobile app?
This flow assumes a mobile app with secure hardware. Web platforms get the same hardware-backed, phishing-resistant step-up using WebAuthn passkeys in the browser, an additive alternative to the device signature that lands on the exact same approval. See Platform Step-Up (WebAuthn / Passkeys).
Which method on which platform
The right verification method follows the platform the player is on. In short:
- • Mobile app & mobile web (iOS 16+ / Android 9+) → a passkey directly (Face ID / Touch ID / fingerprint). Strongest option, zero messaging cost.
- • Desktop web → a passkey directly (Touch ID / Windows Hello).
- • Consoles and native Steam / desktop game clients → the QR device-approval flow, the client can’t run a passkey in-process, so the player approves on their phone. See Device Approval (Consoles & TVs).
- • Email first, SMS on request → the fallback for remote approvals with no passkey path: a signed link to a hosted page goes to the oldest verified address; a text goes out only when there is no verified email, the email could not be delivered, or the player asks for one. SMS is the last resort, not the primary channel.
How the flow works
ENROLL (once, on login)
Your server ──(X-Game-Secret-Key)──▶ POST /api/sdk/player-token ──▶ player token ──▶ device
Device generates a hardware keypair ─▶ POST /api/sdk/device/register (sends the PUBLIC key only)
PER TRANSFER / SEND
Your server starts it as normal (initiate-transfer / initiate-send).
The device polls GET /api/sdk/transfers/pending (the authoritative to-do list)
The player taps approve; the device SIGNS the action and calls
POST /api/sdk/transfers/{id}/approve (self-transfer)
POST /api/sdk/send/{id}/approve (player-to-player send)
For a SEND, the receiver confirms in their app:
POST /api/sdk/send/{id}/confirm-receipt
NO PASSKEY IN THE CLIENT (console / TV / native Steam or desktop)
Same endpoints. The factor comes from the QR device approval grant instead:
POST /api/sdk/approvals/device/begin -> show the QR
POST /api/sdk/approvals/device/poll -> "approved"
then the SAME approve / confirm-receipt call above, with { "device_code": "..." }.
LEGACY FALLBACK (being retired)
A player with no passkey at all can still clear the approval stage with the SMS PIN
(verify-sms). Do not build a new integration on it.A push is not required, the device polls GET /api/sdk/transfers/pending on open and while in the foreground to discover what needs approval. That endpoint is always the source of truth.
Telling a user they have something to collect
A common question: "when a transfer or send arrives, how do I notify the logged-in user there's something to collect?"The pattern is pull for truth, push for wake-up:
In-app: poll the pending list
On app open, on foreground, and on a light interval while active (e.g. every 20 to 30s), call GET /api/sdk/transfers/pending with the player token and render its items:
- •
receiving_confirm→ "You have X to collect" → route to confirm-receipt. Covers both rails: read the item'sflow("send"or"transfer") to pick the endpoint. - •
identity_gate→ "Approve your transfer/send" (your own initiated action).
Safe to drive UI directly, no raw PII (counterparty is a tenant name; amounts + currency only).
Closed app: webhook → your own push
To notify when the app isn't open, your backend subscribes to our webhooks (transfer.claim_pending and the inbound send event) and fires your ownpush (FCM / APNs / web-push). We deliver server-to-server webhooks; we don't push to devices.
The push is a wake-up only, the app must still call /pending for the authoritative list (payloads can be delayed, duplicated, or arrive out of order).
Recommendation: poll-on-open covers the in-app case with zero extra infrastructure; add the webhook→push path only when you need to alert users whose app is backgrounded or closed. Never treat a push payload as the source of truth, re-fetch /pending. See Receiving Webhooks for the event contract.
The auth model (read this first)
Never put your title’s secret key on the device
The X-Game-Secret-Key is server-side only. The device authenticates as the player, using a short-lived player token your server mints for it. A secret on the client is a leaked secret.
| Credential | Lives where | Authenticates | Used for |
|---|---|---|---|
| X-Game-Secret-Key | Your server only | The tenant | Minting player tokens; your existing server APIs |
| player token | The player's device | The player | Every /api/sdk/* call from the device (as Authorization: Bearer) |
The player token is short-lived (15 minutes) and there is no refresh call. On a 401, mint a fresh one via your server's /api/sdk/player-token call and retry, never re-prompt the player just to refresh.
The endpoints
/api/sdk/player-tokenServer mints a short-lived player token (X-Game-Secret-Key)
/api/sdk/device/registerRegister the device + its hardware public key
/api/sdk/transfers/pendingThe authoritative list of actions awaiting this player
/api/sdk/transfers/{id}/approveApprove a self-transfer with a device signature
/api/sdk/send/{id}/approveApprove a player-to-player send (sender side)
/api/sdk/send/{id}/confirm-receiptReceiver confirms an incoming send
/api/transfers/{id}/statusServer-side: detect one transfer’s verification completion (X-Game-Secret-Key)
/api/currency-sends/{id}/statusServer-side: detect one send’s verification completion (X-Game-Secret-Key)
Getting started
- Integrate the Invo plugin / SDK in your client (Unity / Unreal / native).
- On login, have your server mint a player token and the device register itself (Device Enrollment).
- Poll
/api/sdk/transfers/pendingand render the approve / confirm prompts (Approve in App, Collecting). - For consoles, TVs and native Steam or desktop builds, add the QR device approval grant. Same approve call, with the factor obtained on the player's phone.
- Test in sandbox. When you're ready, Invo enables in-app verification for your title, players migrate as they update. The SMS PIN remains only as a retiring fallback for players with no passkey.