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's flow ("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.

CredentialLives whereAuthenticatesUsed for
X-Game-Secret-KeyYour server onlyThe tenantMinting player tokens; your existing server APIs
player tokenThe player's deviceThe playerEvery /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

POST
/api/sdk/player-token

Server mints a short-lived player token (X-Game-Secret-Key)

Device Enrollment
POST
/api/sdk/device/register

Register the device + its hardware public key

Device Enrollment
GET
/api/sdk/transfers/pending

The authoritative list of actions awaiting this player

Approve in App
POST
/api/sdk/transfers/{id}/approve

Approve a self-transfer with a device signature

Approve in App
POST
/api/sdk/send/{id}/approve

Approve a player-to-player send (sender side)

Approve in App
POST
/api/sdk/send/{id}/confirm-receipt

Receiver confirms an incoming send

Confirm Receipt
GET
/api/transfers/{id}/status

Server-side: detect one transfer’s verification completion (X-Game-Secret-Key)

Approve in App
GET
/api/currency-sends/{id}/status

Server-side: detect one send’s verification completion (X-Game-Secret-Key)

Approve in App

Getting started

  1. Integrate the Invo plugin / SDK in your client (Unity / Unreal / native).
  2. On login, have your server mint a player token and the device register itself (Device Enrollment).
  3. Poll /api/sdk/transfers/pending and render the approve / confirm prompts (Approve in App, Collecting).
  4. 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.
  5. 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.