Device Enrollment

Two one-time steps on login: your server mints a short-lived player token for the device, and the device registers itself plus a hardware-backed public key. That key is what every approval signature is verified against.

1. Mint a player token (server-side)

POST

/api/sdk/player-token

Mints a short-lived token for one of your players

Server-side only

This is the one SDK call that uses your game secret. Call it from your server, then hand the returned token to the device. Never ship the secret to the client.

X-Game-Secret-Key: your_game_secret_key_here

Request Body

ParameterTypeRequiredDescription
player_emailstringYesAn existing player in your game

Response

{
  "token": "<player token — hand this to the device>",
  "expires_at": "2026-01-01T12:15:00Z",   // 15-minute lifetime
  "identity_id": "id_9f3c…"                // opaque player identity (safe to store)
}

The raw internal identity is never returned — only the opaque identity_id. A player not found in your game returns 404 player_not_found; a game not yet enabled for in-app verification returns 403 sdk_not_enabled_for_tenant.

2. Register the device (client-side)

POST

/api/sdk/device/register

Registers the device + its hardware public key under the player's identity

Authenticated with the player token

Authorization: Bearer <player token>

Generate the keypair in secure hardware

At registration the plugin generates a keypair inside the device's secure hardware (iOS Secure Enclave / Android StrongBox) and sends only the public key. The private key never leaves the device and is never transmitted or logged.

Request Body

ParameterTypeRequiredDescription
device_fingerprintstringYesA stable per-device identifier (≤128 chars)
device_public_keystringRecommendedPEM or base64-DER public key from the hardware keystore
key_algorithmstringWith keyEC_P256 | ED25519 | RSA_2048
platformstringNoios | android | web | other
rotation_proofobjectOnly to rotateRequired to change an existing key — see "Rotating a key" below

Response

{
  "status": "registered",
  "device": {
    "identity_id": "id_9f3c…",
    "platform": "ios",
    "has_attestation_key": true,
    "key_algorithm": "EC_P256"
  }
}

The device object is a partial view — the four fields above aren't exhaustive. It also includes id, is_active,last_seen_at, and created_at.

The approval signature (device_signal)

Every approval and receipt confirmation carries a device_signal — a signature over the exact action, made with the device's hardware private key. Sign exactly this canonical message:

message   = "{transfer_id}|{nonce}|{timestamp}"     // UTF-8 bytes, pipe-delimited

device_signal = {
  "transfer_id": "<the transaction id being approved>",
  "nonce":       "<fresh random, single-use, base64url, no '|'>",
  "timestamp":   1750000000,        // epoch SECONDS, must be within ±300s of server time
  "signature":   "<base64 signature over message>"
}
  • Algorithms: EC_P256 (ECDSA / SHA-256), ED25519 (raw message), RSA_2048 (PKCS#1 v1.5 / SHA-256) — must match the registered key.
  • • The nonce is single-use; do not reuse it on a retry — generate a fresh signal.
  • • Because the private key never leaves the device, a signal can't be lifted onto another device, and the nonce + timestamp stop replay on the same device.

Rotating a key

Changing an enrolled key needs proof of the old one

The first key for a player enrolls freely. Replacing or adding a different key for a player who already has one requires arotation_proof — a signature made with the current private key over the new key.

  • • A missing proof returns 409 rotation_requires_proof.
  • • An invalid / stale / replayed / malformed proof returns 409 with the specific code ROTATION_PROOF_INVALID | ROTATION_PROOF_STALE | ROTATION_PROOF_REPLAY | ROTATION_PROOF_MALFORMED.
message = "key-rotation|{new_public_key}|{nonce}|{timestamp}"   // signed with the EXISTING key

"rotation_proof": { "nonce": "…", "timestamp": 1750000000, "signature": "<base64>" }

Example — register on login (Unity)

// 1) Your SERVER mints the token (X-Game-Secret-Key) and returns it to the client.
// 2) On the device, the plugin generates the hardware keypair and registers:
IEnumerator RegisterDevice(string playerToken, string deviceFingerprint, string publicKeyPem)
{
    var body = JsonUtility.ToJson(new {
        device_fingerprint = deviceFingerprint,
        device_public_key  = publicKeyPem,
        key_algorithm      = "EC_P256",
        platform           = "ios"
    });
    using (var req = new UnityWebRequest("https://invo.network/api/sdk/device/register", "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();
        // on 401 → mint a fresh token via your server, then retry.
    }
}